Page MenuHomec4science

No OneTemporary

File Metadata

Created
Thu, Sep 19, 12:29
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/resources/celerity/map.php b/resources/celerity/map.php
index 51bc9338d..1f36bf03b 100644
--- a/resources/celerity/map.php
+++ b/resources/celerity/map.php
@@ -1,2356 +1,2406 @@
<?php
/**
* This file is automatically generated. Use 'bin/celerity map' to rebuild it.
*
* @generated
*/
return array(
'names' => array(
'conpherence.pkg.css' => '3c8a0668',
'conpherence.pkg.js' => '020aebcf',
- 'core.pkg.css' => 'e0cb8094',
- 'core.pkg.js' => '5c737607',
- 'differential.pkg.css' => 'b8df73d4',
- 'differential.pkg.js' => '67c9ea4c',
+ 'core.pkg.css' => '7e6e954b',
+ 'core.pkg.js' => 'a747b035',
+ 'differential.pkg.css' => '8d8360fb',
+ 'differential.pkg.js' => '67e02996',
'diffusion.pkg.css' => '42c75c37',
- 'diffusion.pkg.js' => '91192d85',
+ 'diffusion.pkg.js' => 'a98c0bf7',
'maniphest.pkg.css' => '35995d6d',
- 'maniphest.pkg.js' => '286955ae',
+ 'maniphest.pkg.js' => 'c9308721',
'rsrc/audio/basic/alert.mp3' => '17889334',
'rsrc/audio/basic/bing.mp3' => 'a817a0c3',
'rsrc/audio/basic/pock.mp3' => '0fa843d0',
'rsrc/audio/basic/tap.mp3' => '02d16994',
'rsrc/audio/basic/ting.mp3' => 'a6b6540e',
'rsrc/css/aphront/aphront-bars.css' => '4a327b4a',
'rsrc/css/aphront/dark-console.css' => '7f06cda2',
'rsrc/css/aphront/dialog-view.css' => 'b70c70df',
'rsrc/css/aphront/list-filter-view.css' => 'feb64255',
'rsrc/css/aphront/multi-column.css' => 'fbc00ba3',
'rsrc/css/aphront/notification.css' => '30240bd2',
'rsrc/css/aphront/panel-view.css' => '46923d46',
'rsrc/css/aphront/phabricator-nav-view.css' => 'f8a0c1bf',
- 'rsrc/css/aphront/table-view.css' => '76eda3f8',
+ 'rsrc/css/aphront/table-view.css' => '7dc3a9c2',
'rsrc/css/aphront/tokenizer.css' => 'b52d0668',
'rsrc/css/aphront/tooltip.css' => 'e3f2412f',
'rsrc/css/aphront/typeahead-browse.css' => 'b7ed02d2',
'rsrc/css/aphront/typeahead.css' => '8779483d',
'rsrc/css/application/almanac/almanac.css' => '2e050f4f',
'rsrc/css/application/auth/auth.css' => 'add92fd8',
'rsrc/css/application/base/main-menu-view.css' => '8e2d9a28',
- 'rsrc/css/application/base/notification-menu.css' => 'e6962e89',
+ 'rsrc/css/application/base/notification-menu.css' => '4df1ee30',
'rsrc/css/application/base/phui-theme.css' => '35883b37',
'rsrc/css/application/base/standard-page-view.css' => '8a295cb9',
'rsrc/css/application/chatlog/chatlog.css' => 'abdc76ee',
'rsrc/css/application/conduit/conduit-api.css' => 'ce2cfc41',
'rsrc/css/application/config/config-options.css' => '16c920ae',
'rsrc/css/application/config/config-template.css' => '20babf50',
'rsrc/css/application/config/setup-issue.css' => '5eed85b2',
- 'rsrc/css/application/config/unhandled-exception.css' => '9da8fdab',
+ 'rsrc/css/application/config/unhandled-exception.css' => '9ecfc00d',
'rsrc/css/application/conpherence/color.css' => 'b17746b0',
'rsrc/css/application/conpherence/durable-column.css' => '2d57072b',
'rsrc/css/application/conpherence/header-pane.css' => 'c9a3db8e',
'rsrc/css/application/conpherence/menu.css' => '67f4680d',
'rsrc/css/application/conpherence/message-pane.css' => 'd244db1e',
'rsrc/css/application/conpherence/notification.css' => '6a3d4e58',
'rsrc/css/application/conpherence/participant-pane.css' => '69e0058a',
'rsrc/css/application/conpherence/transaction.css' => '3a3f5e7e',
'rsrc/css/application/contentsource/content-source-view.css' => 'cdf0d579',
'rsrc/css/application/countdown/timer.css' => 'bff8012f',
'rsrc/css/application/daemon/bulk-job.css' => '73af99f5',
'rsrc/css/application/dashboard/dashboard.css' => '4267d6c6',
'rsrc/css/application/diff/inline-comment-summary.css' => '81eb368d',
'rsrc/css/application/differential/add-comment.css' => '7e5900d9',
- 'rsrc/css/application/differential/changeset-view.css' => '73660575',
- 'rsrc/css/application/differential/core.css' => 'bdb93065',
+ 'rsrc/css/application/differential/changeset-view.css' => 'bde53589',
+ 'rsrc/css/application/differential/core.css' => '7300a73e',
'rsrc/css/application/differential/phui-inline-comment.css' => '48acce5b',
'rsrc/css/application/differential/revision-comment.css' => '7dbc8d1d',
'rsrc/css/application/differential/revision-history.css' => '8aa3eac5',
'rsrc/css/application/differential/revision-list.css' => '93d2df7d',
'rsrc/css/application/differential/table-of-contents.css' => '0e3364c7',
'rsrc/css/application/diffusion/diffusion-icons.css' => '23b31a1b',
'rsrc/css/application/diffusion/diffusion-readme.css' => 'b68a76e4',
'rsrc/css/application/diffusion/diffusion-repository.css' => 'b89e8c6c',
'rsrc/css/application/diffusion/diffusion.css' => 'b54c77b0',
'rsrc/css/application/feed/feed.css' => 'd8b6e3f8',
'rsrc/css/application/files/global-drag-and-drop.css' => '1d2713a4',
'rsrc/css/application/flag/flag.css' => '2b77be8d',
'rsrc/css/application/harbormaster/harbormaster.css' => '8dfe16b2',
'rsrc/css/application/herald/herald-test.css' => 'e004176f',
'rsrc/css/application/herald/herald.css' => '648d39e2',
'rsrc/css/application/maniphest/report.css' => '3d53188b',
'rsrc/css/application/maniphest/task-edit.css' => '272daa84',
'rsrc/css/application/maniphest/task-summary.css' => '61d1667e',
'rsrc/css/application/objectselector/object-selector.css' => 'ee77366f',
'rsrc/css/application/owners/owners-path-editor.css' => 'fa7c13ef',
'rsrc/css/application/paste/paste.css' => 'b37bcd38',
'rsrc/css/application/people/people-picture-menu-item.css' => 'fe8e07cf',
'rsrc/css/application/people/people-profile.css' => '2ea2daa1',
'rsrc/css/application/phame/phame.css' => '799febf9',
'rsrc/css/application/pholio/pholio-edit.css' => '4df55b3b',
'rsrc/css/application/pholio/pholio-inline-comments.css' => '722b48c2',
'rsrc/css/application/pholio/pholio.css' => '88ef5ef1',
'rsrc/css/application/phortune/phortune-credit-card-form.css' => '3b9868a8',
'rsrc/css/application/phortune/phortune-invoice.css' => '4436b241',
'rsrc/css/application/phortune/phortune.css' => '12e8251a',
'rsrc/css/application/phrequent/phrequent.css' => 'bd79cc67',
'rsrc/css/application/phriction/phriction-document-css.css' => '03380da0',
'rsrc/css/application/policy/policy-edit.css' => '8794e2ed',
'rsrc/css/application/policy/policy-transaction-detail.css' => 'c02b8384',
'rsrc/css/application/policy/policy.css' => 'ceb56a08',
'rsrc/css/application/ponder/ponder-view.css' => '05a09d0a',
- 'rsrc/css/application/project/project-card-view.css' => '3b1f7b20',
+ 'rsrc/css/application/project/project-card-view.css' => '4e7371cd',
+ 'rsrc/css/application/project/project-triggers.css' => 'cb866c2d',
'rsrc/css/application/project/project-view.css' => '567858b3',
'rsrc/css/application/releeph/releeph-core.css' => 'f81ff2db',
'rsrc/css/application/releeph/releeph-preview-branch.css' => '22db5c07',
'rsrc/css/application/releeph/releeph-request-differential-create-dialog.css' => '0ac1ea31',
'rsrc/css/application/releeph/releeph-request-typeahead.css' => 'bce37359',
'rsrc/css/application/search/application-search-view.css' => '0f7c06d8',
'rsrc/css/application/search/search-results.css' => '9ea70ace',
'rsrc/css/application/slowvote/slowvote.css' => '1694baed',
'rsrc/css/application/tokens/tokens.css' => 'ce5a50bd',
'rsrc/css/application/uiexample/example.css' => 'b4795059',
'rsrc/css/core/core.css' => '1b29ed61',
'rsrc/css/core/remarkup.css' => '9e627d41',
- 'rsrc/css/core/syntax.css' => '8a16f91b',
+ 'rsrc/css/core/syntax.css' => '4234f572',
'rsrc/css/core/z-index.css' => '99c0f5eb',
'rsrc/css/diviner/diviner-shared.css' => '4bd263b0',
'rsrc/css/font/font-awesome.css' => '3883938a',
'rsrc/css/font/font-lato.css' => '23631304',
'rsrc/css/font/phui-font-icon-base.css' => 'd7994e06',
'rsrc/css/layout/phabricator-filetree-view.css' => '56cdd875',
'rsrc/css/layout/phabricator-source-code-view.css' => '03d7ac28',
'rsrc/css/phui/button/phui-button-bar.css' => 'a4aa75c4',
'rsrc/css/phui/button/phui-button-simple.css' => '1ff278aa',
'rsrc/css/phui/button/phui-button.css' => 'ea704902',
'rsrc/css/phui/calendar/phui-calendar-day.css' => '9597d706',
'rsrc/css/phui/calendar/phui-calendar-list.css' => 'ccd7e4e2',
'rsrc/css/phui/calendar/phui-calendar-month.css' => 'cb758c42',
'rsrc/css/phui/calendar/phui-calendar.css' => 'f11073aa',
- 'rsrc/css/phui/object-item/phui-oi-big-ui.css' => '9e037c7a',
+ 'rsrc/css/phui/object-item/phui-oi-big-ui.css' => '534f1757',
'rsrc/css/phui/object-item/phui-oi-color.css' => 'b517bfa0',
'rsrc/css/phui/object-item/phui-oi-drag-ui.css' => 'da15d3dc',
'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '490e2e2e',
- 'rsrc/css/phui/object-item/phui-oi-list-view.css' => '909f3844',
+ 'rsrc/css/phui/object-item/phui-oi-list-view.css' => 'a65865a7',
'rsrc/css/phui/object-item/phui-oi-simple-ui.css' => '6a30fa46',
- 'rsrc/css/phui/phui-action-list.css' => 'c1a7631d',
+ 'rsrc/css/phui/phui-action-list.css' => 'c4972757',
'rsrc/css/phui/phui-action-panel.css' => '6c386cbf',
'rsrc/css/phui/phui-badge.css' => '666e25ad',
'rsrc/css/phui/phui-basic-nav-view.css' => '56ebd66d',
'rsrc/css/phui/phui-big-info-view.css' => '362ad37b',
'rsrc/css/phui/phui-box.css' => '5ed3b8cb',
'rsrc/css/phui/phui-bulk-editor.css' => '374d5e30',
'rsrc/css/phui/phui-chart.css' => '7853a69b',
'rsrc/css/phui/phui-cms.css' => '8c05c41e',
'rsrc/css/phui/phui-comment-form.css' => '68a2d99a',
'rsrc/css/phui/phui-comment-panel.css' => 'ec4e31c0',
'rsrc/css/phui/phui-crumbs-view.css' => '614f43cf',
'rsrc/css/phui/phui-curtain-view.css' => '68c5efb6',
'rsrc/css/phui/phui-document-pro.css' => 'b9613a10',
'rsrc/css/phui/phui-document-summary.css' => 'b068eed1',
'rsrc/css/phui/phui-document.css' => '52b748a5',
'rsrc/css/phui/phui-feed-story.css' => 'a0c05029',
'rsrc/css/phui/phui-fontkit.css' => '9b714a5e',
- 'rsrc/css/phui/phui-form-view.css' => '0807e7ac',
+ 'rsrc/css/phui/phui-form-view.css' => '01b796c0',
'rsrc/css/phui/phui-form.css' => '159e2d9c',
'rsrc/css/phui/phui-head-thing.css' => 'd7f293df',
- 'rsrc/css/phui/phui-header-view.css' => '93cea4ec',
+ 'rsrc/css/phui/phui-header-view.css' => '285c9139',
'rsrc/css/phui/phui-hovercard.css' => '6ca90fa0',
'rsrc/css/phui/phui-icon-set-selector.css' => '7aa5f3ec',
- 'rsrc/css/phui/phui-icon.css' => '281f964d',
+ 'rsrc/css/phui/phui-icon.css' => '4cbc684a',
'rsrc/css/phui/phui-image-mask.css' => '62c7f4d2',
'rsrc/css/phui/phui-info-view.css' => '37b8d9ce',
'rsrc/css/phui/phui-invisible-character-view.css' => 'c694c4a4',
'rsrc/css/phui/phui-left-right.css' => '68513c34',
'rsrc/css/phui/phui-lightbox.css' => '4ebf22da',
'rsrc/css/phui/phui-list.css' => '470b1adb',
- 'rsrc/css/phui/phui-object-box.css' => '9b58483d',
+ 'rsrc/css/phui/phui-object-box.css' => 'f434b6be',
'rsrc/css/phui/phui-pager.css' => 'd022c7ad',
'rsrc/css/phui/phui-pinboard-view.css' => '1f08f5d8',
'rsrc/css/phui/phui-property-list-view.css' => 'cad62236',
'rsrc/css/phui/phui-remarkup-preview.css' => '91767007',
'rsrc/css/phui/phui-segment-bar-view.css' => '5166b370',
'rsrc/css/phui/phui-spacing.css' => 'b05cadc3',
'rsrc/css/phui/phui-status.css' => 'e5ff8be0',
- 'rsrc/css/phui/phui-tag-view.css' => 'a42fe34f',
+ 'rsrc/css/phui/phui-tag-view.css' => '29409667',
'rsrc/css/phui/phui-timeline-view.css' => '1e348e4b',
'rsrc/css/phui/phui-two-column-view.css' => '01e6991e',
'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308',
'rsrc/css/phui/workboards/phui-workboard.css' => '74fc9d98',
- 'rsrc/css/phui/workboards/phui-workcard.css' => '8c536f90',
- 'rsrc/css/phui/workboards/phui-workpanel.css' => 'bd546a49',
+ 'rsrc/css/phui/workboards/phui-workcard.css' => '9e9eb0df',
+ 'rsrc/css/phui/workboards/phui-workpanel.css' => '3ae89b20',
'rsrc/css/sprite-login.css' => '18b368a6',
'rsrc/css/sprite-tokens.css' => 'f1896dc5',
'rsrc/css/syntax/syntax-default.css' => '055fc231',
'rsrc/externals/d3/d3.min.js' => 'd67475f5',
'rsrc/externals/font/fontawesome/fontawesome-webfont.eot' => '23f8c698',
'rsrc/externals/font/fontawesome/fontawesome-webfont.ttf' => '70983df0',
'rsrc/externals/font/fontawesome/fontawesome-webfont.woff' => 'cd02f93b',
'rsrc/externals/font/fontawesome/fontawesome-webfont.woff2' => '351fd46a',
'rsrc/externals/font/lato/lato-bold.eot' => '7367aa5e',
'rsrc/externals/font/lato/lato-bold.svg' => '681aa4f5',
'rsrc/externals/font/lato/lato-bold.ttf' => '66d3c296',
'rsrc/externals/font/lato/lato-bold.woff' => '89d9fba7',
'rsrc/externals/font/lato/lato-bold.woff2' => '389fcdb1',
'rsrc/externals/font/lato/lato-bolditalic.eot' => '03eeb4da',
'rsrc/externals/font/lato/lato-bolditalic.svg' => 'f56fa11c',
'rsrc/externals/font/lato/lato-bolditalic.ttf' => '9c3aec21',
'rsrc/externals/font/lato/lato-bolditalic.woff' => 'bfbd0616',
'rsrc/externals/font/lato/lato-bolditalic.woff2' => 'bc7d1274',
'rsrc/externals/font/lato/lato-italic.eot' => '7db5b247',
'rsrc/externals/font/lato/lato-italic.svg' => 'b1ae496f',
'rsrc/externals/font/lato/lato-italic.ttf' => '43eed813',
'rsrc/externals/font/lato/lato-italic.woff' => 'c28975e1',
'rsrc/externals/font/lato/lato-italic.woff2' => 'fffc0d8c',
'rsrc/externals/font/lato/lato-regular.eot' => '06e0c291',
'rsrc/externals/font/lato/lato-regular.svg' => '3ad95f53',
'rsrc/externals/font/lato/lato-regular.ttf' => 'e2e9c398',
'rsrc/externals/font/lato/lato-regular.woff' => '0b13d332',
'rsrc/externals/font/lato/lato-regular.woff2' => '8f846797',
'rsrc/externals/javelin/core/Event.js' => 'c03f2fb4',
'rsrc/externals/javelin/core/Stratcom.js' => '0889b835',
'rsrc/externals/javelin/core/__tests__/event-stop-and-kill.js' => '048472d2',
'rsrc/externals/javelin/core/__tests__/install.js' => '14a7e671',
'rsrc/externals/javelin/core/__tests__/stratcom.js' => 'a28464bb',
'rsrc/externals/javelin/core/__tests__/util.js' => 'e29a4354',
'rsrc/externals/javelin/core/init.js' => '98e6504a',
'rsrc/externals/javelin/core/init_node.js' => '16961339',
'rsrc/externals/javelin/core/install.js' => '5902260c',
'rsrc/externals/javelin/core/util.js' => '22ae1776',
'rsrc/externals/javelin/docs/Base.js' => '5a401d7d',
'rsrc/externals/javelin/docs/onload.js' => 'ee58fb62',
'rsrc/externals/javelin/ext/fx/Color.js' => '78f811c9',
'rsrc/externals/javelin/ext/fx/FX.js' => '34450586',
'rsrc/externals/javelin/ext/reactor/core/DynVal.js' => '202a2e85',
'rsrc/externals/javelin/ext/reactor/core/Reactor.js' => '1c850a26',
'rsrc/externals/javelin/ext/reactor/core/ReactorNode.js' => '72960bc1',
'rsrc/externals/javelin/ext/reactor/core/ReactorNodeCalmer.js' => '225bbb98',
'rsrc/externals/javelin/ext/reactor/dom/RDOM.js' => '6cfa0008',
'rsrc/externals/javelin/ext/view/HTMLView.js' => 'f8c4e135',
'rsrc/externals/javelin/ext/view/View.js' => '289bf236',
'rsrc/externals/javelin/ext/view/ViewInterpreter.js' => '876506b6',
'rsrc/externals/javelin/ext/view/ViewPlaceholder.js' => 'a9942052',
'rsrc/externals/javelin/ext/view/ViewRenderer.js' => '9aae2b66',
'rsrc/externals/javelin/ext/view/ViewVisitor.js' => '308f9fe4',
'rsrc/externals/javelin/ext/view/__tests__/HTMLView.js' => '6e50a13f',
'rsrc/externals/javelin/ext/view/__tests__/View.js' => 'd284be5d',
'rsrc/externals/javelin/ext/view/__tests__/ViewInterpreter.js' => 'a9f35511',
'rsrc/externals/javelin/ext/view/__tests__/ViewRenderer.js' => '3a1b81f6',
'rsrc/externals/javelin/lib/Cookie.js' => '05d290ef',
'rsrc/externals/javelin/lib/DOM.js' => '94681e22',
'rsrc/externals/javelin/lib/History.js' => '030b4f7a',
'rsrc/externals/javelin/lib/JSON.js' => '541f81c3',
'rsrc/externals/javelin/lib/Leader.js' => '0d2490ce',
'rsrc/externals/javelin/lib/Mask.js' => '7c4d8998',
'rsrc/externals/javelin/lib/Quicksand.js' => 'd3799cb4',
'rsrc/externals/javelin/lib/Request.js' => '91863989',
'rsrc/externals/javelin/lib/Resource.js' => '740956e1',
'rsrc/externals/javelin/lib/Routable.js' => '6a18c42e',
'rsrc/externals/javelin/lib/Router.js' => '32755edb',
'rsrc/externals/javelin/lib/Scrollbar.js' => 'a43ae2ae',
- 'rsrc/externals/javelin/lib/Sound.js' => 'e562708c',
+ 'rsrc/externals/javelin/lib/Sound.js' => 'd4cc2d2a',
'rsrc/externals/javelin/lib/URI.js' => '2e255291',
'rsrc/externals/javelin/lib/Vector.js' => 'e9c80beb',
'rsrc/externals/javelin/lib/WebSocket.js' => 'fdc13e4e',
'rsrc/externals/javelin/lib/Workflow.js' => '958e9045',
'rsrc/externals/javelin/lib/__tests__/Cookie.js' => 'ca686f71',
'rsrc/externals/javelin/lib/__tests__/DOM.js' => '4566e249',
'rsrc/externals/javelin/lib/__tests__/JSON.js' => '710377ae',
'rsrc/externals/javelin/lib/__tests__/URI.js' => '6fff0c2b',
'rsrc/externals/javelin/lib/__tests__/behavior.js' => '8426ebeb',
'rsrc/externals/javelin/lib/behavior.js' => 'fce5d170',
'rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js' => '89a1ae3a',
'rsrc/externals/javelin/lib/control/typeahead/Typeahead.js' => 'a4356cde',
'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => 'a241536a',
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js' => '22ee68a5',
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js' => '23387297',
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => '5a79f6c3',
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => '8badee71',
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => '80bff3af',
'rsrc/favicons/favicon-16x16.png' => '4c51a03a',
'rsrc/favicons/mask-icon.svg' => 'db699fe1',
'rsrc/image/BFCFDA.png' => '74b5c88b',
'rsrc/image/actions/edit.png' => 'fd987dff',
'rsrc/image/avatar.png' => '0d17c6c4',
'rsrc/image/checker_dark.png' => '7fc8fa7b',
'rsrc/image/checker_light.png' => '3157a202',
'rsrc/image/checker_lighter.png' => 'c45928c1',
+ 'rsrc/image/chevron-in.png' => '1aa2f88f',
+ 'rsrc/image/chevron-out.png' => 'c815e272',
'rsrc/image/controls/checkbox-checked.png' => '1770d7a0',
'rsrc/image/controls/checkbox-unchecked.png' => 'e1deba0a',
'rsrc/image/d5d8e1.png' => '6764616e',
'rsrc/image/darkload.gif' => '5bd41a89',
'rsrc/image/divot.png' => '0fbe2453',
'rsrc/image/examples/hero.png' => '5d8c4b21',
'rsrc/image/grippy_texture.png' => 'a7d222b5',
'rsrc/image/icon/fatcow/arrow_branch.png' => '98149d9f',
'rsrc/image/icon/fatcow/arrow_merge.png' => 'e142f4f8',
'rsrc/image/icon/fatcow/calendar_edit.png' => '5ff44a08',
'rsrc/image/icon/fatcow/document_black.png' => 'd3515fa5',
'rsrc/image/icon/fatcow/flag_blue.png' => '54db2e5c',
'rsrc/image/icon/fatcow/flag_finish.png' => '2953a51b',
'rsrc/image/icon/fatcow/flag_ghost.png' => '7d9ada92',
'rsrc/image/icon/fatcow/flag_green.png' => '010f7161',
'rsrc/image/icon/fatcow/flag_orange.png' => '6c384ca5',
'rsrc/image/icon/fatcow/flag_pink.png' => '11ac6b12',
'rsrc/image/icon/fatcow/flag_purple.png' => 'c4f423a4',
'rsrc/image/icon/fatcow/flag_red.png' => '9e6d8817',
'rsrc/image/icon/fatcow/flag_yellow.png' => '906733f4',
'rsrc/image/icon/fatcow/key_question.png' => 'c10c26db',
'rsrc/image/icon/fatcow/link.png' => '8edbf327',
'rsrc/image/icon/fatcow/page_white_edit.png' => '17ef5625',
'rsrc/image/icon/fatcow/page_white_put.png' => '82430c91',
'rsrc/image/icon/fatcow/source/conduit.png' => '5b55130c',
'rsrc/image/icon/fatcow/source/email.png' => '8a32b77f',
'rsrc/image/icon/fatcow/source/fax.png' => '8bc2a49b',
'rsrc/image/icon/fatcow/source/mobile.png' => '0a918412',
'rsrc/image/icon/fatcow/source/tablet.png' => 'fc50b050',
'rsrc/image/icon/fatcow/source/web.png' => '70433af3',
'rsrc/image/icon/subscribe.png' => '07ef454e',
'rsrc/image/icon/tango/attachment.png' => 'bac9032d',
'rsrc/image/icon/tango/edit.png' => 'e6296206',
'rsrc/image/icon/tango/go-down.png' => '0b903712',
'rsrc/image/icon/tango/log.png' => '86b6a6f4',
'rsrc/image/icon/tango/upload.png' => '3fe6b92d',
'rsrc/image/icon/unsubscribe.png' => 'db04378a',
'rsrc/image/lightblue-header.png' => 'e6d483c6',
'rsrc/image/logo/light-eye.png' => '72337472',
'rsrc/image/main_texture.png' => '894d03c4',
'rsrc/image/menu_texture.png' => '896c9ade',
'rsrc/image/people/harding.png' => '95b2db63',
'rsrc/image/people/jefferson.png' => 'e883a3a2',
'rsrc/image/people/lincoln.png' => 'be2c07c5',
'rsrc/image/people/mckinley.png' => '6af510a0',
'rsrc/image/people/taft.png' => 'b15ab07e',
'rsrc/image/people/user0.png' => '4bc64b40',
'rsrc/image/people/user1.png' => '8063f445',
'rsrc/image/people/user2.png' => 'd28246c0',
'rsrc/image/people/user3.png' => 'fb1ac12d',
'rsrc/image/people/user4.png' => 'fe4fac8f',
'rsrc/image/people/user5.png' => '3d07065c',
'rsrc/image/people/user6.png' => 'e4bd47c8',
'rsrc/image/people/user7.png' => '71d8fe8b',
'rsrc/image/people/user8.png' => '85f86bf7',
'rsrc/image/people/user9.png' => '523db8aa',
'rsrc/image/people/washington.png' => '86159e68',
'rsrc/image/phrequent_active.png' => 'de66dc50',
'rsrc/image/phrequent_inactive.png' => '79c61baf',
'rsrc/image/resize.png' => '9cc83373',
'rsrc/image/sprite-login-X2.png' => '604545f6',
'rsrc/image/sprite-login.png' => '7a001a9a',
'rsrc/image/sprite-tokens-X2.png' => '21621dd9',
'rsrc/image/sprite-tokens.png' => 'bede2580',
'rsrc/image/texture/card-gradient.png' => 'e6892cb4',
'rsrc/image/texture/dark-menu-hover.png' => '390a4fa1',
'rsrc/image/texture/dark-menu.png' => '542f699c',
'rsrc/image/texture/grip.png' => 'bc80753a',
'rsrc/image/texture/panel-header-gradient.png' => '65004dbf',
'rsrc/image/texture/phlnx-bg.png' => '6c9cd31d',
'rsrc/image/texture/pholio-background.gif' => '84910bfc',
'rsrc/image/texture/table_header.png' => '7652d1ad',
'rsrc/image/texture/table_header_hover.png' => '12ea5236',
'rsrc/image/texture/table_header_tall.png' => '5cc420c4',
'rsrc/js/application/aphlict/Aphlict.js' => '022516b4',
'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => 'e9a2940f',
'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => '4e61fa88',
'rsrc/js/application/aphlict/behavior-aphlict-status.js' => 'c3703a16',
'rsrc/js/application/aphlict/behavior-desktop-notifications-control.js' => '070679fe',
'rsrc/js/application/calendar/behavior-day-view.js' => '727a5a61',
'rsrc/js/application/calendar/behavior-event-all-day.js' => '0b1bc990',
'rsrc/js/application/calendar/behavior-month-view.js' => '158c64e0',
'rsrc/js/application/config/behavior-reorder-fields.js' => '2539f834',
'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => 'aec8e38c',
'rsrc/js/application/conpherence/behavior-conpherence-search.js' => '91befbcc',
'rsrc/js/application/conpherence/behavior-durable-column.js' => 'fa6f30b2',
'rsrc/js/application/conpherence/behavior-menu.js' => '8c2ed2bf',
'rsrc/js/application/conpherence/behavior-participant-pane.js' => '43ba89a2',
'rsrc/js/application/conpherence/behavior-pontificate.js' => '4ae58b5a',
'rsrc/js/application/conpherence/behavior-quicksand-blacklist.js' => '5a6f6a06',
'rsrc/js/application/conpherence/behavior-toggle-widget.js' => '8f959ad0',
'rsrc/js/application/countdown/timer.js' => '6a162524',
'rsrc/js/application/daemon/behavior-bulk-job-reload.js' => '3829a3cf',
'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => '09ecf50c',
'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => '076bd092',
'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '1e413dc9',
'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => '9b1cbd76',
- 'rsrc/js/application/diff/DiffChangeset.js' => 'e7cf10d6',
- 'rsrc/js/application/diff/DiffChangesetList.js' => 'b91204e9',
+ 'rsrc/js/application/diff/DiffChangeset.js' => 'd0a85a85',
+ 'rsrc/js/application/diff/DiffChangesetList.js' => '04023d82',
'rsrc/js/application/diff/DiffInline.js' => 'a4a14a94',
'rsrc/js/application/diff/behavior-preview-link.js' => 'f51e9c17',
'rsrc/js/application/differential/behavior-diff-radios.js' => '925fe8cd',
'rsrc/js/application/differential/behavior-populate.js' => 'dfa1d313',
- 'rsrc/js/application/differential/behavior-user-select.js' => 'e18685c0',
'rsrc/js/application/diffusion/DiffusionLocateFileSource.js' => '94243d89',
'rsrc/js/application/diffusion/behavior-audit-preview.js' => 'b7b73831',
'rsrc/js/application/diffusion/behavior-commit-branches.js' => '4b671572',
- 'rsrc/js/application/diffusion/behavior-commit-graph.js' => '1c88f154',
+ 'rsrc/js/application/diffusion/behavior-commit-graph.js' => 'ef836bf2',
'rsrc/js/application/diffusion/behavior-locate-file.js' => '87428eb2',
'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'c715c123',
'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '6a85bc5a',
'rsrc/js/application/drydock/drydock-live-operation-status.js' => '47a0728b',
'rsrc/js/application/files/behavior-document-engine.js' => '243d6c22',
'rsrc/js/application/files/behavior-icon-composer.js' => '38a6cedb',
'rsrc/js/application/files/behavior-launch-icon-composer.js' => 'a17b84f1',
'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => 'b347a301',
'rsrc/js/application/herald/HeraldRuleEditor.js' => '27daef73',
'rsrc/js/application/herald/PathTypeahead.js' => 'ad486db3',
'rsrc/js/application/herald/herald-rule-editor.js' => '0922e81d',
- 'rsrc/js/application/maniphest/behavior-batch-selector.js' => 'cffd39b4',
+ 'rsrc/js/application/maniphest/behavior-batch-selector.js' => '139ef688',
'rsrc/js/application/maniphest/behavior-line-chart.js' => 'c8147a20',
'rsrc/js/application/maniphest/behavior-list-edit.js' => 'c687e867',
- 'rsrc/js/application/maniphest/behavior-subpriorityeditor.js' => '8400307c',
'rsrc/js/application/owners/OwnersPathEditor.js' => '2a8b62d9',
'rsrc/js/application/owners/owners-path-editor.js' => 'ff688a7a',
'rsrc/js/application/passphrase/passphrase-credential-control.js' => '48fe33d0',
'rsrc/js/application/pholio/behavior-pholio-mock-edit.js' => '3eed1f2b',
'rsrc/js/application/pholio/behavior-pholio-mock-view.js' => '5aa1544e',
'rsrc/js/application/phortune/behavior-stripe-payment-form.js' => '02cb4398',
'rsrc/js/application/phortune/behavior-test-payment-form.js' => '4a7fb02b',
'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f',
'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9',
'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172',
- 'rsrc/js/application/projects/WorkboardBoard.js' => '45d0b2b1',
- 'rsrc/js/application/projects/WorkboardCard.js' => '9a513421',
- 'rsrc/js/application/projects/WorkboardColumn.js' => '8573dc1b',
+ 'rsrc/js/application/projects/WorkboardBoard.js' => 'c02a5497',
+ 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8',
+ 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4',
+ 'rsrc/js/application/projects/WorkboardColumn.js' => 'c3d24e63',
'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7',
- 'rsrc/js/application/projects/behavior-project-boards.js' => '05c74d65',
+ 'rsrc/js/application/projects/WorkboardDropEffect.js' => '8e0aa661',
+ 'rsrc/js/application/projects/WorkboardHeader.js' => '111bfd2d',
+ 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'ebe83a6b',
+ 'rsrc/js/application/projects/WorkboardOrderTemplate.js' => '03e8891f',
+ 'rsrc/js/application/projects/behavior-project-boards.js' => 'aad45445',
'rsrc/js/application/projects/behavior-project-create.js' => '34c53422',
'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9',
'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68',
'rsrc/js/application/releeph/releeph-request-state-change.js' => '9f081f05',
'rsrc/js/application/releeph/releeph-request-typeahead.js' => 'aa3a100c',
- 'rsrc/js/application/repository/repository-crossreference.js' => 'db0c0214',
+ 'rsrc/js/application/repository/repository-crossreference.js' => 'c15122b4',
'rsrc/js/application/search/behavior-reorder-profile-menu-items.js' => 'e5bdb730',
'rsrc/js/application/search/behavior-reorder-queries.js' => 'b86f297f',
'rsrc/js/application/transactions/behavior-comment-actions.js' => '4dffaeb2',
'rsrc/js/application/transactions/behavior-reorder-configs.js' => '4842f137',
'rsrc/js/application/transactions/behavior-reorder-fields.js' => '0ad8d31f',
'rsrc/js/application/transactions/behavior-show-older-transactions.js' => '600f440c',
'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => '2bdadf1a',
'rsrc/js/application/transactions/behavior-transaction-list.js' => '9cec214e',
+ 'rsrc/js/application/trigger/TriggerRule.js' => '1c60c3fc',
+ 'rsrc/js/application/trigger/TriggerRuleControl.js' => '5faf27b9',
+ 'rsrc/js/application/trigger/TriggerRuleEditor.js' => 'b49fd60c',
+ 'rsrc/js/application/trigger/TriggerRuleType.js' => '4feea7d3',
+ 'rsrc/js/application/trigger/trigger-rule-editor.js' => '398fdf13',
'rsrc/js/application/typeahead/behavior-typeahead-browse.js' => '70245195',
'rsrc/js/application/typeahead/behavior-typeahead-search.js' => '7b139193',
'rsrc/js/application/uiexample/gesture-example.js' => '242dedd0',
'rsrc/js/application/uiexample/notification-example.js' => '29819b75',
'rsrc/js/core/Busy.js' => '5202e831',
'rsrc/js/core/DragAndDropFileUpload.js' => '4370900d',
- 'rsrc/js/core/DraggableList.js' => '3c6bd549',
+ 'rsrc/js/core/DraggableList.js' => 'c9ad6f70',
'rsrc/js/core/Favicon.js' => '7930776a',
'rsrc/js/core/FileUpload.js' => 'ab85e184',
'rsrc/js/core/Hovercard.js' => '074f0783',
'rsrc/js/core/KeyboardShortcut.js' => 'c9749dcd',
'rsrc/js/core/KeyboardShortcutManager.js' => '37b8a04a',
'rsrc/js/core/MultirowRowManager.js' => '5b54c823',
'rsrc/js/core/Notification.js' => 'a9b91e3f',
- 'rsrc/js/core/Prefab.js' => 'bf457520',
+ 'rsrc/js/core/Prefab.js' => '5793d835',
'rsrc/js/core/ShapedRequest.js' => 'abf88db8',
'rsrc/js/core/TextAreaUtils.js' => 'f340a484',
'rsrc/js/core/Title.js' => '43bc9360',
'rsrc/js/core/ToolTip.js' => '83754533',
'rsrc/js/core/behavior-active-nav.js' => '7353f43d',
'rsrc/js/core/behavior-audio-source.js' => '3dc5ad43',
'rsrc/js/core/behavior-autofocus.js' => '65bb0011',
'rsrc/js/core/behavior-badge-view.js' => '92cdd7b6',
'rsrc/js/core/behavior-bulk-editor.js' => 'aa6d2308',
'rsrc/js/core/behavior-choose-control.js' => '04f8a1e3',
'rsrc/js/core/behavior-copy.js' => 'cf32921f',
'rsrc/js/core/behavior-detect-timezone.js' => '78bc5d94',
'rsrc/js/core/behavior-device.js' => '0cf79f45',
'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '7ad020a5',
'rsrc/js/core/behavior-fancy-datepicker.js' => '956f3eeb',
'rsrc/js/core/behavior-file-tree.js' => 'ee82cedb',
'rsrc/js/core/behavior-form.js' => '55d7b788',
'rsrc/js/core/behavior-gesture.js' => 'b58d1a2a',
'rsrc/js/core/behavior-global-drag-and-drop.js' => '1cab0e9a',
'rsrc/js/core/behavior-high-security-warning.js' => 'dae2d55b',
'rsrc/js/core/behavior-history-install.js' => '6a1583a8',
'rsrc/js/core/behavior-hovercard.js' => '6c379000',
'rsrc/js/core/behavior-keyboard-pager.js' => '1325b731',
'rsrc/js/core/behavior-keyboard-shortcuts.js' => '2cc87f49',
'rsrc/js/core/behavior-lightbox-attachments.js' => 'c7e748bf',
'rsrc/js/core/behavior-line-linker.js' => 'e15c8b1f',
'rsrc/js/core/behavior-linked-container.js' => '74446546',
'rsrc/js/core/behavior-more.js' => '506aa3f4',
'rsrc/js/core/behavior-object-selector.js' => 'a4af0b4a',
- 'rsrc/js/core/behavior-oncopy.js' => '418f6684',
+ 'rsrc/js/core/behavior-oncopy.js' => 'ff7b3f22',
'rsrc/js/core/behavior-phabricator-nav.js' => 'f166c949',
'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => '2f80333f',
'rsrc/js/core/behavior-read-only-warning.js' => 'b9109f8f',
'rsrc/js/core/behavior-redirect.js' => '407ee861',
'rsrc/js/core/behavior-refresh-csrf.js' => '46116c01',
'rsrc/js/core/behavior-remarkup-load-image.js' => '202bfa3f',
'rsrc/js/core/behavior-remarkup-preview.js' => 'd8a86cfb',
'rsrc/js/core/behavior-reorder-applications.js' => 'aa371860',
'rsrc/js/core/behavior-reveal-content.js' => 'b105a3a6',
'rsrc/js/core/behavior-scrollbar.js' => '92388bae',
'rsrc/js/core/behavior-search-typeahead.js' => '1cb7d027',
'rsrc/js/core/behavior-select-content.js' => 'e8240b50',
'rsrc/js/core/behavior-select-on-click.js' => '66365ee2',
'rsrc/js/core/behavior-setup-check-https.js' => '01384686',
'rsrc/js/core/behavior-time-typeahead.js' => '5803b9e7',
- 'rsrc/js/core/behavior-toggle-class.js' => 'f5c78ae3',
+ 'rsrc/js/core/behavior-toggle-class.js' => '32db8374',
'rsrc/js/core/behavior-tokenizer.js' => '3b4899b0',
'rsrc/js/core/behavior-tooltip.js' => '73ecc1f8',
'rsrc/js/core/behavior-user-menu.js' => '60cd9241',
'rsrc/js/core/behavior-watch-anchor.js' => '0e6d261f',
'rsrc/js/core/behavior-workflow.js' => '9623adc1',
'rsrc/js/core/darkconsole/DarkLog.js' => '3b869402',
'rsrc/js/core/darkconsole/DarkMessage.js' => '26cd4b73',
'rsrc/js/core/darkconsole/behavior-dark-console.js' => 'f39d968b',
'rsrc/js/core/phtize.js' => '2f1db1ed',
'rsrc/js/phui/behavior-phui-dropdown-menu.js' => '5cf0501a',
'rsrc/js/phui/behavior-phui-file-upload.js' => 'e150bd50',
'rsrc/js/phui/behavior-phui-selectable-list.js' => 'b26a41e4',
'rsrc/js/phui/behavior-phui-submenu.js' => 'b5e9bff9',
'rsrc/js/phui/behavior-phui-tab-group.js' => '242aa08b',
+ 'rsrc/js/phui/behavior-phui-timer-control.js' => 'f84bcbf4',
'rsrc/js/phuix/PHUIXActionListView.js' => 'c68f183f',
'rsrc/js/phuix/PHUIXActionView.js' => 'aaa08f3b',
- 'rsrc/js/phuix/PHUIXAutocomplete.js' => '58cc4ab8',
+ 'rsrc/js/phuix/PHUIXAutocomplete.js' => '8f139ef0',
'rsrc/js/phuix/PHUIXButtonView.js' => '55a24e84',
'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bdce4d78',
'rsrc/js/phuix/PHUIXExample.js' => 'c2c500a7',
'rsrc/js/phuix/PHUIXFormControl.js' => '38c1f3fb',
'rsrc/js/phuix/PHUIXIconView.js' => 'a5257c4e',
),
'symbols' => array(
'almanac-css' => '2e050f4f',
'aphront-bars' => '4a327b4a',
'aphront-dark-console-css' => '7f06cda2',
'aphront-dialog-view-css' => 'b70c70df',
'aphront-list-filter-view-css' => 'feb64255',
'aphront-multi-column-view-css' => 'fbc00ba3',
'aphront-panel-view-css' => '46923d46',
- 'aphront-table-view-css' => '76eda3f8',
+ 'aphront-table-view-css' => '7dc3a9c2',
'aphront-tokenizer-control-css' => 'b52d0668',
'aphront-tooltip-css' => 'e3f2412f',
'aphront-typeahead-control-css' => '8779483d',
'application-search-view-css' => '0f7c06d8',
'auth-css' => 'add92fd8',
'bulk-job-css' => '73af99f5',
'conduit-api-css' => 'ce2cfc41',
'config-options-css' => '16c920ae',
'conpherence-color-css' => 'b17746b0',
'conpherence-durable-column-view' => '2d57072b',
'conpherence-header-pane-css' => 'c9a3db8e',
'conpherence-menu-css' => '67f4680d',
'conpherence-message-pane-css' => 'd244db1e',
'conpherence-notification-css' => '6a3d4e58',
'conpherence-participant-pane-css' => '69e0058a',
'conpherence-thread-manager' => 'aec8e38c',
'conpherence-transaction-css' => '3a3f5e7e',
'd3' => 'd67475f5',
- 'differential-changeset-view-css' => '73660575',
- 'differential-core-view-css' => 'bdb93065',
+ 'differential-changeset-view-css' => 'bde53589',
+ 'differential-core-view-css' => '7300a73e',
'differential-revision-add-comment-css' => '7e5900d9',
'differential-revision-comment-css' => '7dbc8d1d',
'differential-revision-history-css' => '8aa3eac5',
'differential-revision-list-css' => '93d2df7d',
'differential-table-of-contents-css' => '0e3364c7',
'diffusion-css' => 'b54c77b0',
'diffusion-icons-css' => '23b31a1b',
'diffusion-readme-css' => 'b68a76e4',
'diffusion-repository-css' => 'b89e8c6c',
'diviner-shared-css' => '4bd263b0',
'font-fontawesome' => '3883938a',
'font-lato' => '23631304',
'global-drag-and-drop-css' => '1d2713a4',
'harbormaster-css' => '8dfe16b2',
'herald-css' => '648d39e2',
'herald-rule-editor' => '27daef73',
'herald-test-css' => 'e004176f',
'inline-comment-summary-css' => '81eb368d',
'javelin-aphlict' => '022516b4',
'javelin-behavior' => 'fce5d170',
'javelin-behavior-aphlict-dropdown' => 'e9a2940f',
'javelin-behavior-aphlict-listen' => '4e61fa88',
'javelin-behavior-aphlict-status' => 'c3703a16',
'javelin-behavior-aphront-basic-tokenizer' => '3b4899b0',
'javelin-behavior-aphront-drag-and-drop-textarea' => '7ad020a5',
'javelin-behavior-aphront-form-disable-on-submit' => '55d7b788',
'javelin-behavior-aphront-more' => '506aa3f4',
'javelin-behavior-audio-source' => '3dc5ad43',
'javelin-behavior-audit-preview' => 'b7b73831',
'javelin-behavior-badge-view' => '92cdd7b6',
'javelin-behavior-bulk-editor' => 'aa6d2308',
'javelin-behavior-bulk-job-reload' => '3829a3cf',
'javelin-behavior-calendar-month-view' => '158c64e0',
'javelin-behavior-choose-control' => '04f8a1e3',
'javelin-behavior-comment-actions' => '4dffaeb2',
'javelin-behavior-config-reorder-fields' => '2539f834',
'javelin-behavior-conpherence-menu' => '8c2ed2bf',
'javelin-behavior-conpherence-participant-pane' => '43ba89a2',
'javelin-behavior-conpherence-pontificate' => '4ae58b5a',
'javelin-behavior-conpherence-search' => '91befbcc',
'javelin-behavior-countdown-timer' => '6a162524',
'javelin-behavior-dark-console' => 'f39d968b',
'javelin-behavior-dashboard-async-panel' => '09ecf50c',
'javelin-behavior-dashboard-move-panels' => '076bd092',
'javelin-behavior-dashboard-query-panel-select' => '1e413dc9',
'javelin-behavior-dashboard-tab-panel' => '9b1cbd76',
'javelin-behavior-day-view' => '727a5a61',
'javelin-behavior-desktop-notifications-control' => '070679fe',
'javelin-behavior-detect-timezone' => '78bc5d94',
'javelin-behavior-device' => '0cf79f45',
'javelin-behavior-diff-preview-link' => 'f51e9c17',
'javelin-behavior-differential-diff-radios' => '925fe8cd',
'javelin-behavior-differential-populate' => 'dfa1d313',
- 'javelin-behavior-differential-user-select' => 'e18685c0',
'javelin-behavior-diffusion-commit-branches' => '4b671572',
- 'javelin-behavior-diffusion-commit-graph' => '1c88f154',
+ 'javelin-behavior-diffusion-commit-graph' => 'ef836bf2',
'javelin-behavior-diffusion-locate-file' => '87428eb2',
'javelin-behavior-diffusion-pull-lastmodified' => 'c715c123',
'javelin-behavior-document-engine' => '243d6c22',
'javelin-behavior-doorkeeper-tag' => '6a85bc5a',
'javelin-behavior-drydock-live-operation-status' => '47a0728b',
'javelin-behavior-durable-column' => 'fa6f30b2',
'javelin-behavior-editengine-reorder-configs' => '4842f137',
'javelin-behavior-editengine-reorder-fields' => '0ad8d31f',
'javelin-behavior-event-all-day' => '0b1bc990',
'javelin-behavior-fancy-datepicker' => '956f3eeb',
'javelin-behavior-global-drag-and-drop' => '1cab0e9a',
'javelin-behavior-harbormaster-log' => 'b347a301',
'javelin-behavior-herald-rule-editor' => '0922e81d',
'javelin-behavior-high-security-warning' => 'dae2d55b',
'javelin-behavior-history-install' => '6a1583a8',
'javelin-behavior-icon-composer' => '38a6cedb',
'javelin-behavior-launch-icon-composer' => 'a17b84f1',
'javelin-behavior-lightbox-attachments' => 'c7e748bf',
'javelin-behavior-line-chart' => 'c8147a20',
'javelin-behavior-linked-container' => '74446546',
- 'javelin-behavior-maniphest-batch-selector' => 'cffd39b4',
+ 'javelin-behavior-maniphest-batch-selector' => '139ef688',
'javelin-behavior-maniphest-list-editor' => 'c687e867',
- 'javelin-behavior-maniphest-subpriority-editor' => '8400307c',
'javelin-behavior-owners-path-editor' => 'ff688a7a',
'javelin-behavior-passphrase-credential-control' => '48fe33d0',
'javelin-behavior-phabricator-active-nav' => '7353f43d',
'javelin-behavior-phabricator-autofocus' => '65bb0011',
'javelin-behavior-phabricator-clipboard-copy' => 'cf32921f',
'javelin-behavior-phabricator-file-tree' => 'ee82cedb',
'javelin-behavior-phabricator-gesture' => 'b58d1a2a',
'javelin-behavior-phabricator-gesture-example' => '242dedd0',
'javelin-behavior-phabricator-keyboard-pager' => '1325b731',
'javelin-behavior-phabricator-keyboard-shortcuts' => '2cc87f49',
'javelin-behavior-phabricator-line-linker' => 'e15c8b1f',
'javelin-behavior-phabricator-nav' => 'f166c949',
'javelin-behavior-phabricator-notification-example' => '29819b75',
'javelin-behavior-phabricator-object-selector' => 'a4af0b4a',
- 'javelin-behavior-phabricator-oncopy' => '418f6684',
+ 'javelin-behavior-phabricator-oncopy' => 'ff7b3f22',
'javelin-behavior-phabricator-remarkup-assist' => '2f80333f',
'javelin-behavior-phabricator-reveal-content' => 'b105a3a6',
'javelin-behavior-phabricator-search-typeahead' => '1cb7d027',
'javelin-behavior-phabricator-show-older-transactions' => '600f440c',
'javelin-behavior-phabricator-tooltips' => '73ecc1f8',
'javelin-behavior-phabricator-transaction-comment-form' => '2bdadf1a',
'javelin-behavior-phabricator-transaction-list' => '9cec214e',
'javelin-behavior-phabricator-watch-anchor' => '0e6d261f',
'javelin-behavior-pholio-mock-edit' => '3eed1f2b',
'javelin-behavior-pholio-mock-view' => '5aa1544e',
'javelin-behavior-phui-dropdown-menu' => '5cf0501a',
'javelin-behavior-phui-file-upload' => 'e150bd50',
'javelin-behavior-phui-hovercards' => '6c379000',
'javelin-behavior-phui-selectable-list' => 'b26a41e4',
'javelin-behavior-phui-submenu' => 'b5e9bff9',
'javelin-behavior-phui-tab-group' => '242aa08b',
+ 'javelin-behavior-phui-timer-control' => 'f84bcbf4',
'javelin-behavior-phuix-example' => 'c2c500a7',
'javelin-behavior-policy-control' => '0eaa33a9',
'javelin-behavior-policy-rule-editor' => '9347f172',
- 'javelin-behavior-project-boards' => '05c74d65',
+ 'javelin-behavior-project-boards' => 'aad45445',
'javelin-behavior-project-create' => '34c53422',
'javelin-behavior-quicksand-blacklist' => '5a6f6a06',
'javelin-behavior-read-only-warning' => 'b9109f8f',
'javelin-behavior-redirect' => '407ee861',
'javelin-behavior-refresh-csrf' => '46116c01',
'javelin-behavior-releeph-preview-branch' => '75184d68',
'javelin-behavior-releeph-request-state-change' => '9f081f05',
'javelin-behavior-releeph-request-typeahead' => 'aa3a100c',
'javelin-behavior-remarkup-load-image' => '202bfa3f',
'javelin-behavior-remarkup-preview' => 'd8a86cfb',
'javelin-behavior-reorder-applications' => 'aa371860',
'javelin-behavior-reorder-columns' => '8ac32fd9',
'javelin-behavior-reorder-profile-menu-items' => 'e5bdb730',
- 'javelin-behavior-repository-crossreference' => 'db0c0214',
+ 'javelin-behavior-repository-crossreference' => 'c15122b4',
'javelin-behavior-scrollbar' => '92388bae',
'javelin-behavior-search-reorder-queries' => 'b86f297f',
'javelin-behavior-select-content' => 'e8240b50',
'javelin-behavior-select-on-click' => '66365ee2',
'javelin-behavior-setup-check-https' => '01384686',
'javelin-behavior-stripe-payment-form' => '02cb4398',
'javelin-behavior-test-payment-form' => '4a7fb02b',
'javelin-behavior-time-typeahead' => '5803b9e7',
- 'javelin-behavior-toggle-class' => 'f5c78ae3',
+ 'javelin-behavior-toggle-class' => '32db8374',
'javelin-behavior-toggle-widget' => '8f959ad0',
+ 'javelin-behavior-trigger-rule-editor' => '398fdf13',
'javelin-behavior-typeahead-browse' => '70245195',
'javelin-behavior-typeahead-search' => '7b139193',
'javelin-behavior-user-menu' => '60cd9241',
'javelin-behavior-view-placeholder' => 'a9942052',
'javelin-behavior-workflow' => '9623adc1',
'javelin-color' => '78f811c9',
'javelin-cookie' => '05d290ef',
'javelin-diffusion-locate-file-source' => '94243d89',
'javelin-dom' => '94681e22',
'javelin-dynval' => '202a2e85',
'javelin-event' => 'c03f2fb4',
'javelin-fx' => '34450586',
'javelin-history' => '030b4f7a',
'javelin-install' => '5902260c',
'javelin-json' => '541f81c3',
'javelin-leader' => '0d2490ce',
'javelin-magical-init' => '98e6504a',
'javelin-mask' => '7c4d8998',
'javelin-quicksand' => 'd3799cb4',
'javelin-reactor' => '1c850a26',
'javelin-reactor-dom' => '6cfa0008',
'javelin-reactor-node-calmer' => '225bbb98',
'javelin-reactornode' => '72960bc1',
'javelin-request' => '91863989',
'javelin-resource' => '740956e1',
'javelin-routable' => '6a18c42e',
'javelin-router' => '32755edb',
'javelin-scrollbar' => 'a43ae2ae',
- 'javelin-sound' => 'e562708c',
+ 'javelin-sound' => 'd4cc2d2a',
'javelin-stratcom' => '0889b835',
'javelin-tokenizer' => '89a1ae3a',
'javelin-typeahead' => 'a4356cde',
'javelin-typeahead-composite-source' => '22ee68a5',
'javelin-typeahead-normalizer' => 'a241536a',
'javelin-typeahead-ondemand-source' => '23387297',
'javelin-typeahead-preloaded-source' => '5a79f6c3',
'javelin-typeahead-source' => '8badee71',
'javelin-typeahead-static-source' => '80bff3af',
'javelin-uri' => '2e255291',
'javelin-util' => '22ae1776',
'javelin-vector' => 'e9c80beb',
'javelin-view' => '289bf236',
'javelin-view-html' => 'f8c4e135',
'javelin-view-interpreter' => '876506b6',
'javelin-view-renderer' => '9aae2b66',
'javelin-view-visitor' => '308f9fe4',
'javelin-websocket' => 'fdc13e4e',
- 'javelin-workboard-board' => '45d0b2b1',
- 'javelin-workboard-card' => '9a513421',
- 'javelin-workboard-column' => '8573dc1b',
+ 'javelin-workboard-board' => 'c02a5497',
+ 'javelin-workboard-card' => '0392a5d8',
+ 'javelin-workboard-card-template' => '2a61f8d4',
+ 'javelin-workboard-column' => 'c3d24e63',
'javelin-workboard-controller' => '42c7a5a7',
+ 'javelin-workboard-drop-effect' => '8e0aa661',
+ 'javelin-workboard-header' => '111bfd2d',
+ 'javelin-workboard-header-template' => 'ebe83a6b',
+ 'javelin-workboard-order-template' => '03e8891f',
'javelin-workflow' => '958e9045',
'maniphest-report-css' => '3d53188b',
'maniphest-task-edit-css' => '272daa84',
'maniphest-task-summary-css' => '61d1667e',
'multirow-row-manager' => '5b54c823',
'owners-path-editor' => '2a8b62d9',
'owners-path-editor-css' => 'fa7c13ef',
'paste-css' => 'b37bcd38',
'path-typeahead' => 'ad486db3',
'people-picture-menu-item-css' => 'fe8e07cf',
'people-profile-css' => '2ea2daa1',
- 'phabricator-action-list-view-css' => 'c1a7631d',
+ 'phabricator-action-list-view-css' => 'c4972757',
'phabricator-busy' => '5202e831',
'phabricator-chatlog-css' => 'abdc76ee',
'phabricator-content-source-view-css' => 'cdf0d579',
'phabricator-core-css' => '1b29ed61',
'phabricator-countdown-css' => 'bff8012f',
'phabricator-darklog' => '3b869402',
'phabricator-darkmessage' => '26cd4b73',
'phabricator-dashboard-css' => '4267d6c6',
- 'phabricator-diff-changeset' => 'e7cf10d6',
- 'phabricator-diff-changeset-list' => 'b91204e9',
+ 'phabricator-diff-changeset' => 'd0a85a85',
+ 'phabricator-diff-changeset-list' => '04023d82',
'phabricator-diff-inline' => 'a4a14a94',
'phabricator-drag-and-drop-file-upload' => '4370900d',
- 'phabricator-draggable-list' => '3c6bd549',
+ 'phabricator-draggable-list' => 'c9ad6f70',
'phabricator-fatal-config-template-css' => '20babf50',
'phabricator-favicon' => '7930776a',
'phabricator-feed-css' => 'd8b6e3f8',
'phabricator-file-upload' => 'ab85e184',
'phabricator-filetree-view-css' => '56cdd875',
'phabricator-flag-css' => '2b77be8d',
'phabricator-keyboard-shortcut' => 'c9749dcd',
'phabricator-keyboard-shortcut-manager' => '37b8a04a',
'phabricator-main-menu-view' => '8e2d9a28',
'phabricator-nav-view-css' => 'f8a0c1bf',
'phabricator-notification' => 'a9b91e3f',
'phabricator-notification-css' => '30240bd2',
- 'phabricator-notification-menu-css' => 'e6962e89',
+ 'phabricator-notification-menu-css' => '4df1ee30',
'phabricator-object-selector-css' => 'ee77366f',
'phabricator-phtize' => '2f1db1ed',
- 'phabricator-prefab' => 'bf457520',
+ 'phabricator-prefab' => '5793d835',
'phabricator-remarkup-css' => '9e627d41',
'phabricator-search-results-css' => '9ea70ace',
'phabricator-shaped-request' => 'abf88db8',
'phabricator-slowvote-css' => '1694baed',
'phabricator-source-code-view-css' => '03d7ac28',
'phabricator-standard-page-view' => '8a295cb9',
'phabricator-textareautils' => 'f340a484',
'phabricator-title' => '43bc9360',
'phabricator-tooltip' => '83754533',
'phabricator-ui-example-css' => 'b4795059',
'phabricator-zindex-css' => '99c0f5eb',
'phame-css' => '799febf9',
'pholio-css' => '88ef5ef1',
'pholio-edit-css' => '4df55b3b',
'pholio-inline-comments-css' => '722b48c2',
'phortune-credit-card-form' => 'd12d214f',
'phortune-credit-card-form-css' => '3b9868a8',
'phortune-css' => '12e8251a',
'phortune-invoice-css' => '4436b241',
'phrequent-css' => 'bd79cc67',
'phriction-document-css' => '03380da0',
'phui-action-panel-css' => '6c386cbf',
'phui-badge-view-css' => '666e25ad',
'phui-basic-nav-view-css' => '56ebd66d',
'phui-big-info-view-css' => '362ad37b',
'phui-box-css' => '5ed3b8cb',
'phui-bulk-editor-css' => '374d5e30',
'phui-button-bar-css' => 'a4aa75c4',
'phui-button-css' => 'ea704902',
'phui-button-simple-css' => '1ff278aa',
'phui-calendar-css' => 'f11073aa',
'phui-calendar-day-css' => '9597d706',
'phui-calendar-list-css' => 'ccd7e4e2',
'phui-calendar-month-css' => 'cb758c42',
'phui-chart-css' => '7853a69b',
'phui-cms-css' => '8c05c41e',
'phui-comment-form-css' => '68a2d99a',
'phui-comment-panel-css' => 'ec4e31c0',
'phui-crumbs-view-css' => '614f43cf',
'phui-curtain-view-css' => '68c5efb6',
'phui-document-summary-view-css' => 'b068eed1',
'phui-document-view-css' => '52b748a5',
'phui-document-view-pro-css' => 'b9613a10',
'phui-feed-story-css' => 'a0c05029',
'phui-font-icon-base-css' => 'd7994e06',
'phui-fontkit-css' => '9b714a5e',
'phui-form-css' => '159e2d9c',
- 'phui-form-view-css' => '0807e7ac',
+ 'phui-form-view-css' => '01b796c0',
'phui-head-thing-view-css' => 'd7f293df',
- 'phui-header-view-css' => '93cea4ec',
+ 'phui-header-view-css' => '285c9139',
'phui-hovercard' => '074f0783',
'phui-hovercard-view-css' => '6ca90fa0',
'phui-icon-set-selector-css' => '7aa5f3ec',
- 'phui-icon-view-css' => '281f964d',
+ 'phui-icon-view-css' => '4cbc684a',
'phui-image-mask-css' => '62c7f4d2',
'phui-info-view-css' => '37b8d9ce',
'phui-inline-comment-view-css' => '48acce5b',
'phui-invisible-character-view-css' => 'c694c4a4',
'phui-left-right-css' => '68513c34',
'phui-lightbox-css' => '4ebf22da',
'phui-list-view-css' => '470b1adb',
- 'phui-object-box-css' => '9b58483d',
- 'phui-oi-big-ui-css' => '9e037c7a',
+ 'phui-object-box-css' => 'f434b6be',
+ 'phui-oi-big-ui-css' => '534f1757',
'phui-oi-color-css' => 'b517bfa0',
'phui-oi-drag-ui-css' => 'da15d3dc',
'phui-oi-flush-ui-css' => '490e2e2e',
- 'phui-oi-list-view-css' => '909f3844',
+ 'phui-oi-list-view-css' => 'a65865a7',
'phui-oi-simple-ui-css' => '6a30fa46',
'phui-pager-css' => 'd022c7ad',
'phui-pinboard-view-css' => '1f08f5d8',
'phui-property-list-view-css' => 'cad62236',
'phui-remarkup-preview-css' => '91767007',
'phui-segment-bar-view-css' => '5166b370',
'phui-spacing-css' => 'b05cadc3',
'phui-status-list-view-css' => 'e5ff8be0',
- 'phui-tag-view-css' => 'a42fe34f',
+ 'phui-tag-view-css' => '29409667',
'phui-theme-css' => '35883b37',
'phui-timeline-view-css' => '1e348e4b',
'phui-two-column-view-css' => '01e6991e',
'phui-workboard-color-css' => 'e86de308',
'phui-workboard-view-css' => '74fc9d98',
- 'phui-workcard-view-css' => '8c536f90',
- 'phui-workpanel-view-css' => 'bd546a49',
+ 'phui-workcard-view-css' => '9e9eb0df',
+ 'phui-workpanel-view-css' => '3ae89b20',
'phuix-action-list-view' => 'c68f183f',
'phuix-action-view' => 'aaa08f3b',
- 'phuix-autocomplete' => '58cc4ab8',
+ 'phuix-autocomplete' => '8f139ef0',
'phuix-button-view' => '55a24e84',
'phuix-dropdown-menu' => 'bdce4d78',
'phuix-form-control-view' => '38c1f3fb',
'phuix-icon-view' => 'a5257c4e',
'policy-css' => 'ceb56a08',
'policy-edit-css' => '8794e2ed',
'policy-transaction-detail-css' => 'c02b8384',
'ponder-view-css' => '05a09d0a',
- 'project-card-view-css' => '3b1f7b20',
+ 'project-card-view-css' => '4e7371cd',
+ 'project-triggers-css' => 'cb866c2d',
'project-view-css' => '567858b3',
'releeph-core' => 'f81ff2db',
'releeph-preview-branch' => '22db5c07',
'releeph-request-differential-create-dialog' => '0ac1ea31',
'releeph-request-typeahead-css' => 'bce37359',
'setup-issue-css' => '5eed85b2',
'sprite-login-css' => '18b368a6',
'sprite-tokens-css' => 'f1896dc5',
'syntax-default-css' => '055fc231',
- 'syntax-highlighting-css' => '8a16f91b',
+ 'syntax-highlighting-css' => '4234f572',
'tokens-css' => 'ce5a50bd',
+ 'trigger-rule' => '1c60c3fc',
+ 'trigger-rule-control' => '5faf27b9',
+ 'trigger-rule-editor' => 'b49fd60c',
+ 'trigger-rule-type' => '4feea7d3',
'typeahead-browse-css' => 'b7ed02d2',
- 'unhandled-exception-css' => '9da8fdab',
+ 'unhandled-exception-css' => '9ecfc00d',
),
'requires' => array(
'01384686' => array(
'javelin-behavior',
'javelin-uri',
'phabricator-notification',
),
'022516b4' => array(
'javelin-install',
'javelin-util',
'javelin-websocket',
'javelin-leader',
'javelin-json',
),
'02cb4398' => array(
'javelin-behavior',
'javelin-dom',
'phortune-credit-card-form',
),
'030b4f7a' => array(
'javelin-stratcom',
'javelin-install',
'javelin-uri',
'javelin-util',
),
+ '0392a5d8' => array(
+ 'javelin-install',
+ ),
+ '03e8891f' => array(
+ 'javelin-install',
+ ),
+ '04023d82' => array(
+ 'javelin-install',
+ 'phuix-button-view',
+ ),
'04f8a1e3' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-workflow',
),
- '05c74d65' => array(
- 'javelin-behavior',
- 'javelin-dom',
- 'javelin-util',
- 'javelin-vector',
- 'javelin-stratcom',
- 'javelin-workflow',
- 'javelin-workboard-controller',
- ),
'05d290ef' => array(
'javelin-install',
'javelin-util',
),
'070679fe' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-uri',
'phabricator-notification',
),
'074f0783' => array(
'javelin-install',
'javelin-dom',
'javelin-vector',
'javelin-request',
'javelin-uri',
),
'076bd092' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-workflow',
'phabricator-draggable-list',
),
'0889b835' => array(
'javelin-install',
'javelin-event',
'javelin-util',
'javelin-magical-init',
),
'0922e81d' => array(
'herald-rule-editor',
'javelin-behavior',
),
'09ecf50c' => array(
'javelin-behavior',
'javelin-dom',
'javelin-workflow',
),
'0ad8d31f' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'phabricator-draggable-list',
),
'0cf79f45' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-vector',
'javelin-install',
),
'0d2490ce' => array(
'javelin-install',
),
'0e6d261f' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-vector',
),
'0eaa33a9' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'phuix-dropdown-menu',
'phuix-action-list-view',
'phuix-action-view',
'javelin-workflow',
'phuix-icon-view',
),
+ '111bfd2d' => array(
+ 'javelin-install',
+ ),
'1325b731' => array(
'javelin-behavior',
'javelin-uri',
'phabricator-keyboard-shortcut',
),
- '1c850a26' => array(
- 'javelin-install',
- 'javelin-util',
- ),
- '1c88f154' => array(
+ '139ef688' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
+ 'javelin-util',
+ ),
+ '1c850a26' => array(
+ 'javelin-install',
+ 'javelin-util',
),
'1cab0e9a' => array(
'javelin-behavior',
'javelin-dom',
'javelin-uri',
'javelin-mask',
'phabricator-drag-and-drop-file-upload',
),
'1cb7d027' => array(
'javelin-behavior',
'javelin-typeahead-ondemand-source',
'javelin-typeahead',
'javelin-dom',
'javelin-uri',
'javelin-util',
'javelin-stratcom',
'phabricator-prefab',
'phuix-icon-view',
),
'1e413dc9' => array(
'javelin-behavior',
'javelin-dom',
),
'1ff278aa' => array(
'phui-button-css',
),
'202a2e85' => array(
'javelin-install',
'javelin-reactornode',
'javelin-util',
'javelin-reactor',
),
'202bfa3f' => array(
'javelin-behavior',
'javelin-request',
),
'225bbb98' => array(
'javelin-install',
'javelin-reactor',
'javelin-util',
),
'22ee68a5' => array(
'javelin-install',
'javelin-typeahead-source',
'javelin-util',
),
23387297 => array(
'javelin-install',
'javelin-util',
'javelin-request',
'javelin-typeahead-source',
),
23631304 => array(
'phui-fontkit-css',
),
'242aa08b' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'242dedd0' => array(
'javelin-stratcom',
'javelin-behavior',
'javelin-vector',
'javelin-dom',
),
'243d6c22' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
),
'2539f834' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-json',
'phabricator-draggable-list',
),
'27daef73' => array(
'multirow-row-manager',
'javelin-install',
'javelin-util',
'javelin-dom',
'javelin-stratcom',
'javelin-json',
'phabricator-prefab',
),
'289bf236' => array(
'javelin-install',
'javelin-util',
),
'29819b75' => array(
'phabricator-notification',
'javelin-stratcom',
'javelin-behavior',
),
+ '2a61f8d4' => array(
+ 'javelin-install',
+ ),
'2a8b62d9' => array(
'multirow-row-manager',
'javelin-install',
'path-typeahead',
'javelin-dom',
'javelin-util',
'phabricator-prefab',
'phuix-form-control-view',
),
'2bdadf1a' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-request',
'phabricator-shaped-request',
),
'2cc87f49' => array(
'javelin-behavior',
'javelin-workflow',
'javelin-json',
'javelin-dom',
'phabricator-keyboard-shortcut',
),
'2e255291' => array(
'javelin-install',
'javelin-util',
'javelin-stratcom',
),
'2f1db1ed' => array(
'javelin-util',
),
'2f80333f' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'phabricator-phtize',
'phabricator-textareautils',
'javelin-workflow',
'javelin-vector',
'phuix-autocomplete',
'javelin-mask',
),
'308f9fe4' => array(
'javelin-install',
'javelin-util',
),
'32755edb' => array(
'javelin-install',
'javelin-util',
),
+ '32db8374' => array(
+ 'javelin-behavior',
+ 'javelin-stratcom',
+ 'javelin-dom',
+ ),
34450586 => array(
'javelin-color',
'javelin-install',
'javelin-util',
),
'34c53422' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-workflow',
),
'37b8a04a' => array(
'javelin-install',
'javelin-util',
'javelin-stratcom',
'javelin-dom',
'javelin-vector',
),
'3829a3cf' => array(
'javelin-behavior',
'javelin-uri',
),
'38a6cedb' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
),
'38c1f3fb' => array(
'javelin-install',
'javelin-dom',
),
+ '398fdf13' => array(
+ 'javelin-behavior',
+ 'trigger-rule-editor',
+ 'trigger-rule',
+ 'trigger-rule-type',
+ ),
+ '3ae89b20' => array(
+ 'phui-workcard-view-css',
+ ),
'3b4899b0' => array(
'javelin-behavior',
'phabricator-prefab',
),
- '3c6bd549' => array(
- 'javelin-install',
- 'javelin-dom',
- 'javelin-stratcom',
- 'javelin-util',
- 'javelin-vector',
- 'javelin-magical-init',
- ),
'3dc5ad43' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-vector',
'javelin-dom',
),
'3eed1f2b' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-workflow',
'javelin-quicksand',
'phabricator-phtize',
'phabricator-drag-and-drop-file-upload',
'phabricator-draggable-list',
),
'407ee861' => array(
'javelin-behavior',
'javelin-uri',
),
- '418f6684' => array(
- 'javelin-behavior',
- 'javelin-dom',
+ '4234f572' => array(
+ 'syntax-default-css',
),
'42c7a5a7' => array(
'javelin-install',
'javelin-dom',
'javelin-util',
'javelin-vector',
'javelin-stratcom',
'javelin-workflow',
'phabricator-drag-and-drop-file-upload',
'javelin-workboard-board',
),
'4370900d' => array(
'javelin-install',
'javelin-util',
'javelin-request',
'javelin-dom',
'javelin-uri',
'phabricator-file-upload',
),
'43ba89a2' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-workflow',
'javelin-util',
'phabricator-notification',
'conpherence-thread-manager',
),
'43bc9360' => array(
'javelin-install',
),
- '45d0b2b1' => array(
- 'javelin-install',
- 'javelin-dom',
- 'javelin-util',
- 'javelin-stratcom',
- 'javelin-workflow',
- 'phabricator-draggable-list',
- 'javelin-workboard-column',
- ),
'46116c01' => array(
'javelin-request',
'javelin-behavior',
'javelin-dom',
'javelin-router',
'javelin-util',
'phabricator-busy',
),
'47a0728b' => array(
'javelin-behavior',
'javelin-dom',
'javelin-request',
),
'4842f137' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'phabricator-draggable-list',
),
'48fe33d0' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-workflow',
'javelin-util',
'javelin-uri',
),
'490e2e2e' => array(
'phui-oi-list-view-css',
),
'4a7fb02b' => array(
'javelin-behavior',
'javelin-dom',
'phortune-credit-card-form',
),
'4ae58b5a' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-workflow',
'javelin-stratcom',
'conpherence-thread-manager',
),
'4b671572' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-request',
),
'4dffaeb2' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'phuix-form-control-view',
'phuix-icon-view',
'javelin-behavior-phabricator-gesture',
),
'4e61fa88' => array(
'javelin-behavior',
'javelin-aphlict',
'javelin-stratcom',
'javelin-request',
'javelin-uri',
'javelin-dom',
'javelin-json',
'javelin-router',
'javelin-util',
'javelin-leader',
'javelin-sound',
'phabricator-notification',
),
+ '4feea7d3' => array(
+ 'trigger-rule-control',
+ ),
'506aa3f4' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'5202e831' => array(
'javelin-install',
'javelin-dom',
'javelin-fx',
),
+ '534f1757' => array(
+ 'phui-oi-list-view-css',
+ ),
'541f81c3' => array(
'javelin-install',
),
'55a24e84' => array(
'javelin-install',
'javelin-dom',
),
'55d7b788' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
+ '5793d835' => array(
+ 'javelin-install',
+ 'javelin-util',
+ 'javelin-dom',
+ 'javelin-typeahead',
+ 'javelin-tokenizer',
+ 'javelin-typeahead-preloaded-source',
+ 'javelin-typeahead-ondemand-source',
+ 'javelin-dom',
+ 'javelin-stratcom',
+ 'javelin-util',
+ ),
'5803b9e7' => array(
'javelin-behavior',
'javelin-util',
'javelin-dom',
'javelin-stratcom',
'javelin-vector',
'javelin-typeahead-static-source',
),
- '58cc4ab8' => array(
- 'javelin-install',
- 'javelin-dom',
- 'phuix-icon-view',
- 'phabricator-prefab',
- ),
'5902260c' => array(
'javelin-util',
'javelin-magical-init',
),
'5a6f6a06' => array(
'javelin-behavior',
'javelin-quicksand',
),
'5a79f6c3' => array(
'javelin-install',
'javelin-util',
'javelin-request',
'javelin-typeahead-source',
),
'5aa1544e' => array(
'javelin-behavior',
'javelin-util',
'javelin-stratcom',
'javelin-dom',
'javelin-vector',
'javelin-magical-init',
'javelin-request',
'javelin-history',
'javelin-workflow',
'javelin-mask',
'javelin-behavior-device',
'phabricator-keyboard-shortcut',
),
'5b54c823' => array(
'javelin-install',
'javelin-stratcom',
'javelin-dom',
'javelin-util',
),
'5cf0501a' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'phuix-dropdown-menu',
),
+ '5faf27b9' => array(
+ 'phuix-form-control-view',
+ ),
'600f440c' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'phabricator-busy',
),
'60cd9241' => array(
'javelin-behavior',
),
'65bb0011' => array(
'javelin-behavior',
'javelin-dom',
),
'66365ee2' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'6a1583a8' => array(
'javelin-behavior',
'javelin-history',
),
'6a162524' => array(
'javelin-behavior',
'javelin-dom',
),
'6a18c42e' => array(
'javelin-install',
),
'6a30fa46' => array(
'phui-oi-list-view-css',
),
'6a85bc5a' => array(
'javelin-behavior',
'javelin-dom',
'javelin-json',
'javelin-workflow',
'javelin-magical-init',
),
'6c379000' => array(
'javelin-behavior',
'javelin-behavior-device',
'javelin-stratcom',
'javelin-vector',
'phui-hovercard',
),
'6cfa0008' => array(
'javelin-dom',
'javelin-dynval',
'javelin-reactor',
'javelin-reactornode',
'javelin-install',
'javelin-util',
),
70245195 => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
),
'727a5a61' => array(
'phuix-icon-view',
),
'72960bc1' => array(
'javelin-install',
'javelin-reactor',
'javelin-util',
'javelin-reactor-node-calmer',
),
'7353f43d' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-vector',
'javelin-dom',
'javelin-uri',
),
- 73660575 => array(
- 'phui-inline-comment-view-css',
- ),
'73ecc1f8' => array(
'javelin-behavior',
'javelin-behavior-device',
'javelin-stratcom',
'phabricator-tooltip',
),
'740956e1' => array(
'javelin-util',
'javelin-uri',
'javelin-install',
),
74446546 => array(
'javelin-behavior',
'javelin-dom',
),
'75184d68' => array(
'javelin-behavior',
'javelin-dom',
'javelin-uri',
'javelin-request',
),
'78bc5d94' => array(
'javelin-behavior',
'javelin-uri',
'phabricator-notification',
),
'78f811c9' => array(
'javelin-install',
),
'7930776a' => array(
'javelin-install',
'javelin-dom',
),
'7ad020a5' => array(
'javelin-behavior',
'javelin-dom',
'phabricator-drag-and-drop-file-upload',
'phabricator-textareautils',
),
'7b139193' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
),
'7c4d8998' => array(
'javelin-install',
'javelin-dom',
),
'80bff3af' => array(
'javelin-install',
'javelin-typeahead-source',
),
83754533 => array(
'javelin-install',
'javelin-util',
'javelin-dom',
'javelin-vector',
),
- '8400307c' => array(
- 'javelin-behavior',
- 'javelin-dom',
- 'javelin-stratcom',
- 'javelin-workflow',
- 'phabricator-draggable-list',
- ),
- '8573dc1b' => array(
- 'javelin-install',
- 'javelin-workboard-card',
- ),
'87428eb2' => array(
'javelin-behavior',
'javelin-diffusion-locate-file-source',
'javelin-dom',
'javelin-typeahead',
'javelin-uri',
),
'876506b6' => array(
'javelin-view',
'javelin-install',
'javelin-dom',
),
'89a1ae3a' => array(
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-install',
),
- '8a16f91b' => array(
- 'syntax-default-css',
- ),
'8ac32fd9' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'phabricator-draggable-list',
),
'8badee71' => array(
'javelin-install',
'javelin-util',
'javelin-dom',
'javelin-typeahead-normalizer',
),
'8c2ed2bf' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-workflow',
'javelin-behavior-device',
'javelin-history',
'javelin-vector',
'javelin-scrollbar',
'phabricator-title',
'phabricator-shaped-request',
'conpherence-thread-manager',
),
+ '8e0aa661' => array(
+ 'javelin-install',
+ 'javelin-dom',
+ ),
'8e2d9a28' => array(
'phui-theme-css',
),
+ '8f139ef0' => array(
+ 'javelin-install',
+ 'javelin-dom',
+ 'phuix-icon-view',
+ 'phabricator-prefab',
+ ),
'8f959ad0' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-workflow',
'javelin-stratcom',
),
91863989 => array(
'javelin-install',
'javelin-stratcom',
'javelin-util',
'javelin-behavior',
'javelin-json',
'javelin-dom',
'javelin-resource',
'javelin-routable',
),
'91befbcc' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-workflow',
'javelin-stratcom',
),
'92388bae' => array(
'javelin-behavior',
'javelin-scrollbar',
),
'925fe8cd' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'92cdd7b6' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'9347f172' => array(
'javelin-behavior',
'multirow-row-manager',
'javelin-dom',
'javelin-util',
'phabricator-prefab',
'javelin-json',
),
'94243d89' => array(
'javelin-install',
'javelin-dom',
'javelin-typeahead-preloaded-source',
'javelin-util',
),
'94681e22' => array(
'javelin-magical-init',
'javelin-install',
'javelin-util',
'javelin-vector',
'javelin-stratcom',
),
'956f3eeb' => array(
'javelin-behavior',
'javelin-util',
'javelin-dom',
'javelin-stratcom',
'javelin-vector',
),
'958e9045' => array(
'javelin-stratcom',
'javelin-request',
'javelin-dom',
'javelin-vector',
'javelin-install',
'javelin-util',
'javelin-mask',
'javelin-uri',
'javelin-routable',
),
'9623adc1' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'javelin-router',
),
- '9a513421' => array(
- 'javelin-install',
- ),
'9aae2b66' => array(
'javelin-install',
'javelin-util',
),
'9b1cbd76' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
),
'9cec214e' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'javelin-uri',
'phabricator-textareautils',
),
- '9e037c7a' => array(
- 'phui-oi-list-view-css',
- ),
'9f081f05' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-workflow',
'javelin-util',
'phabricator-keyboard-shortcut',
),
'a17b84f1' => array(
'javelin-behavior',
'javelin-dom',
'javelin-workflow',
),
'a241536a' => array(
'javelin-install',
),
'a4356cde' => array(
'javelin-install',
'javelin-dom',
'javelin-vector',
'javelin-util',
),
'a43ae2ae' => array(
'javelin-install',
'javelin-dom',
'javelin-stratcom',
'javelin-vector',
),
'a4a14a94' => array(
'javelin-dom',
),
'a4aa75c4' => array(
'phui-button-css',
'phui-button-simple-css',
),
'a4af0b4a' => array(
'javelin-behavior',
'javelin-dom',
'javelin-request',
'javelin-util',
),
'a5257c4e' => array(
'javelin-install',
'javelin-dom',
),
'a9942052' => array(
'javelin-behavior',
'javelin-dom',
'javelin-view-renderer',
'javelin-install',
),
'a9b91e3f' => array(
'javelin-install',
'javelin-dom',
'javelin-stratcom',
'javelin-util',
'phabricator-notification-css',
),
'aa371860' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'phabricator-draggable-list',
),
'aa3a100c' => array(
'javelin-behavior',
'javelin-dom',
'javelin-typeahead',
'javelin-typeahead-ondemand-source',
'javelin-dom',
),
'aa6d2308' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'multirow-row-manager',
'javelin-json',
'phuix-form-control-view',
),
'aaa08f3b' => array(
'javelin-install',
'javelin-dom',
'javelin-util',
),
+ 'aad45445' => array(
+ 'javelin-behavior',
+ 'javelin-dom',
+ 'javelin-util',
+ 'javelin-vector',
+ 'javelin-stratcom',
+ 'javelin-workflow',
+ 'javelin-workboard-controller',
+ 'javelin-workboard-drop-effect',
+ ),
'ab85e184' => array(
'javelin-install',
'javelin-dom',
'phabricator-notification',
),
'abf88db8' => array(
'javelin-install',
'javelin-util',
'javelin-request',
'javelin-router',
),
'ad486db3' => array(
'javelin-install',
'javelin-typeahead',
'javelin-dom',
'javelin-request',
'javelin-typeahead-ondemand-source',
'javelin-util',
),
'aec8e38c' => array(
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-install',
'javelin-aphlict',
'javelin-workflow',
'javelin-router',
'javelin-behavior-device',
'javelin-vector',
),
'b105a3a6' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'b26a41e4' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'b347a301' => array(
'javelin-behavior',
),
+ 'b49fd60c' => array(
+ 'multirow-row-manager',
+ 'trigger-rule',
+ ),
'b517bfa0' => array(
'phui-oi-list-view-css',
),
'b52d0668' => array(
'aphront-typeahead-control-css',
'phui-tag-view-css',
),
'b58d1a2a' => array(
'javelin-behavior',
'javelin-behavior-device',
'javelin-stratcom',
'javelin-vector',
'javelin-dom',
'javelin-magical-init',
),
'b5e9bff9' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'b7b73831' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'phabricator-shaped-request',
),
'b86f297f' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'phabricator-draggable-list',
),
'b9109f8f' => array(
'javelin-behavior',
'javelin-uri',
'phabricator-notification',
),
- 'b91204e9' => array(
- 'javelin-install',
- 'phuix-button-view',
- ),
- 'bd546a49' => array(
- 'phui-workcard-view-css',
- ),
'bdce4d78' => array(
'javelin-install',
'javelin-util',
'javelin-dom',
'javelin-vector',
'javelin-stratcom',
),
- 'bf457520' => array(
+ 'bde53589' => array(
+ 'phui-inline-comment-view-css',
+ ),
+ 'c02a5497' => array(
'javelin-install',
- 'javelin-util',
- 'javelin-dom',
- 'javelin-typeahead',
- 'javelin-tokenizer',
- 'javelin-typeahead-preloaded-source',
- 'javelin-typeahead-ondemand-source',
'javelin-dom',
- 'javelin-stratcom',
'javelin-util',
+ 'javelin-stratcom',
+ 'javelin-workflow',
+ 'phabricator-draggable-list',
+ 'javelin-workboard-column',
+ 'javelin-workboard-header-template',
+ 'javelin-workboard-card-template',
+ 'javelin-workboard-order-template',
),
'c03f2fb4' => array(
'javelin-install',
),
+ 'c15122b4' => array(
+ 'javelin-behavior',
+ 'javelin-dom',
+ 'javelin-stratcom',
+ 'javelin-uri',
+ ),
'c2c500a7' => array(
'javelin-install',
'javelin-dom',
'phuix-button-view',
),
'c3703a16' => array(
'javelin-behavior',
'javelin-aphlict',
'phabricator-phtize',
'javelin-dom',
),
+ 'c3d24e63' => array(
+ 'javelin-install',
+ 'javelin-workboard-card',
+ 'javelin-workboard-header',
+ ),
'c687e867' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-workflow',
'javelin-fx',
'javelin-util',
),
'c68f183f' => array(
'javelin-install',
'javelin-dom',
),
'c715c123' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'javelin-workflow',
'javelin-json',
),
'c7e748bf' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-mask',
'javelin-util',
'phuix-icon-view',
'phabricator-busy',
),
'c8147a20' => array(
'javelin-behavior',
'javelin-dom',
'javelin-vector',
'phui-chart-css',
),
'c9749dcd' => array(
'javelin-install',
'javelin-util',
'phabricator-keyboard-shortcut-manager',
),
- 'cf32921f' => array(
- 'javelin-behavior',
+ 'c9ad6f70' => array(
+ 'javelin-install',
'javelin-dom',
'javelin-stratcom',
+ 'javelin-util',
+ 'javelin-vector',
+ 'javelin-magical-init',
),
- 'cffd39b4' => array(
+ 'cf32921f' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
+ ),
+ 'd0a85a85' => array(
+ 'javelin-dom',
'javelin-util',
+ 'javelin-stratcom',
+ 'javelin-install',
+ 'javelin-workflow',
+ 'javelin-router',
+ 'javelin-behavior-device',
+ 'javelin-vector',
+ 'phabricator-diff-inline',
),
'd12d214f' => array(
'javelin-install',
'javelin-dom',
'javelin-json',
'javelin-workflow',
'javelin-util',
),
'd3799cb4' => array(
'javelin-install',
),
+ 'd4cc2d2a' => array(
+ 'javelin-install',
+ ),
'd8a86cfb' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'phabricator-shaped-request',
),
'da15d3dc' => array(
'phui-oi-list-view-css',
),
'dae2d55b' => array(
'javelin-behavior',
'javelin-uri',
'phabricator-notification',
),
- 'db0c0214' => array(
- 'javelin-behavior',
- 'javelin-dom',
- 'javelin-stratcom',
- 'javelin-uri',
- ),
'dfa1d313' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'phabricator-tooltip',
'phabricator-diff-changeset-list',
'phabricator-diff-changeset',
),
'e150bd50' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'phuix-dropdown-menu',
),
'e15c8b1f' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-history',
),
- 'e18685c0' => array(
- 'javelin-behavior',
- 'javelin-dom',
- 'javelin-stratcom',
- ),
- 'e562708c' => array(
- 'javelin-install',
- ),
'e5bdb730' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'phabricator-draggable-list',
),
- 'e7cf10d6' => array(
- 'javelin-dom',
- 'javelin-util',
- 'javelin-stratcom',
- 'javelin-install',
- 'javelin-workflow',
- 'javelin-router',
- 'javelin-behavior-device',
- 'javelin-vector',
- 'phabricator-diff-inline',
- ),
'e8240b50' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'e9a2940f' => array(
'javelin-behavior',
'javelin-request',
'javelin-stratcom',
'javelin-vector',
'javelin-dom',
'javelin-uri',
'javelin-behavior-device',
'phabricator-title',
'phabricator-favicon',
),
'e9c80beb' => array(
'javelin-install',
'javelin-event',
),
+ 'ebe83a6b' => array(
+ 'javelin-install',
+ ),
'ec4e31c0' => array(
'phui-timeline-view-css',
),
'ee77366f' => array(
'aphront-dialog-view-css',
),
'ee82cedb' => array(
'javelin-behavior',
'phabricator-keyboard-shortcut',
'javelin-stratcom',
),
+ 'ef836bf2' => array(
+ 'javelin-behavior',
+ 'javelin-dom',
+ 'javelin-stratcom',
+ ),
'f166c949' => array(
'javelin-behavior',
'javelin-behavior-device',
'javelin-stratcom',
'javelin-dom',
'javelin-magical-init',
'javelin-vector',
'javelin-request',
'javelin-util',
),
'f340a484' => array(
'javelin-install',
'javelin-dom',
'javelin-vector',
),
'f39d968b' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-util',
'javelin-dom',
'javelin-request',
'phabricator-keyboard-shortcut',
'phabricator-darklog',
'phabricator-darkmessage',
),
'f51e9c17' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
- 'f5c78ae3' => array(
+ 'f84bcbf4' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
),
'f8c4e135' => array(
'javelin-install',
'javelin-dom',
'javelin-view-visitor',
'javelin-util',
),
'fa6f30b2' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
'javelin-behavior-device',
'javelin-scrollbar',
'javelin-quicksand',
'phabricator-keyboard-shortcut',
'conpherence-thread-manager',
),
'fce5d170' => array(
'javelin-magical-init',
'javelin-util',
),
'fdc13e4e' => array(
'javelin-install',
),
'ff688a7a' => array(
'owners-path-editor',
'javelin-behavior',
),
+ 'ff7b3f22' => array(
+ 'javelin-behavior',
+ 'javelin-dom',
+ ),
),
'packages' => array(
'conpherence.pkg.css' => array(
'conpherence-durable-column-view',
'conpherence-menu-css',
'conpherence-color-css',
'conpherence-message-pane-css',
'conpherence-notification-css',
'conpherence-transaction-css',
'conpherence-participant-pane-css',
'conpherence-header-pane-css',
),
'conpherence.pkg.js' => array(
'javelin-behavior-conpherence-menu',
'javelin-behavior-conpherence-participant-pane',
'javelin-behavior-conpherence-pontificate',
'javelin-behavior-toggle-widget',
),
'core.pkg.css' => array(
'phabricator-core-css',
'phabricator-zindex-css',
'phui-button-css',
'phui-button-simple-css',
'phui-theme-css',
'phabricator-standard-page-view',
'aphront-dialog-view-css',
'phui-form-view-css',
'aphront-panel-view-css',
'aphront-table-view-css',
'aphront-tokenizer-control-css',
'aphront-typeahead-control-css',
'aphront-list-filter-view-css',
'application-search-view-css',
'phabricator-remarkup-css',
'syntax-highlighting-css',
'syntax-default-css',
'phui-pager-css',
'aphront-tooltip-css',
'phabricator-flag-css',
'phui-info-view-css',
'phabricator-main-menu-view',
'phabricator-notification-css',
'phabricator-notification-menu-css',
'phui-lightbox-css',
'phui-comment-panel-css',
'phui-header-view-css',
'phabricator-nav-view-css',
'phui-basic-nav-view-css',
'phui-crumbs-view-css',
'phui-oi-list-view-css',
'phui-oi-color-css',
'phui-oi-big-ui-css',
'phui-oi-drag-ui-css',
'phui-oi-simple-ui-css',
'phui-oi-flush-ui-css',
'global-drag-and-drop-css',
'phui-spacing-css',
'phui-form-css',
'phui-icon-view-css',
'phabricator-action-list-view-css',
'phui-property-list-view-css',
'phui-tag-view-css',
'phui-list-view-css',
'font-fontawesome',
'font-lato',
'phui-font-icon-base-css',
'phui-fontkit-css',
'phui-box-css',
'phui-object-box-css',
'phui-timeline-view-css',
'phui-two-column-view-css',
'phui-curtain-view-css',
'sprite-login-css',
'sprite-tokens-css',
'tokens-css',
'auth-css',
'phui-status-list-view-css',
'phui-feed-story-css',
'phabricator-feed-css',
'phabricator-dashboard-css',
'aphront-multi-column-view-css',
),
'core.pkg.js' => array(
'javelin-util',
'javelin-install',
'javelin-event',
'javelin-stratcom',
'javelin-behavior',
'javelin-resource',
'javelin-request',
'javelin-vector',
'javelin-dom',
'javelin-json',
'javelin-uri',
'javelin-workflow',
'javelin-mask',
'javelin-typeahead',
'javelin-typeahead-normalizer',
'javelin-typeahead-source',
'javelin-typeahead-preloaded-source',
'javelin-typeahead-ondemand-source',
'javelin-tokenizer',
'javelin-history',
'javelin-router',
'javelin-routable',
'javelin-behavior-aphront-basic-tokenizer',
'javelin-behavior-workflow',
'javelin-behavior-aphront-form-disable-on-submit',
'phabricator-keyboard-shortcut-manager',
'phabricator-keyboard-shortcut',
'javelin-behavior-phabricator-keyboard-shortcuts',
'javelin-behavior-refresh-csrf',
'javelin-behavior-phabricator-watch-anchor',
'javelin-behavior-phabricator-autofocus',
'phuix-dropdown-menu',
'phuix-action-list-view',
'phuix-action-view',
'phuix-icon-view',
'phabricator-phtize',
'javelin-behavior-phabricator-oncopy',
'phabricator-tooltip',
'javelin-behavior-phabricator-tooltips',
'phabricator-prefab',
'javelin-behavior-device',
'javelin-behavior-toggle-class',
'javelin-behavior-lightbox-attachments',
'phabricator-busy',
'javelin-sound',
'javelin-aphlict',
'phabricator-notification',
'javelin-behavior-aphlict-listen',
'javelin-behavior-phabricator-search-typeahead',
'javelin-behavior-aphlict-dropdown',
'javelin-behavior-history-install',
'javelin-behavior-phabricator-gesture',
'javelin-behavior-phabricator-active-nav',
'javelin-behavior-phabricator-nav',
'javelin-behavior-phabricator-remarkup-assist',
'phabricator-textareautils',
'phabricator-file-upload',
'javelin-behavior-global-drag-and-drop',
'javelin-behavior-phabricator-reveal-content',
'phui-hovercard',
'javelin-behavior-phui-hovercards',
'javelin-color',
'javelin-fx',
'phabricator-draggable-list',
'javelin-behavior-phabricator-transaction-list',
'javelin-behavior-phabricator-show-older-transactions',
'javelin-behavior-phui-dropdown-menu',
'javelin-behavior-doorkeeper-tag',
'phabricator-title',
'javelin-leader',
'javelin-websocket',
'javelin-behavior-dashboard-async-panel',
'javelin-behavior-dashboard-tab-panel',
'javelin-quicksand',
'javelin-behavior-quicksand-blacklist',
'javelin-behavior-high-security-warning',
'javelin-behavior-read-only-warning',
'javelin-scrollbar',
'javelin-behavior-scrollbar',
'javelin-behavior-durable-column',
'conpherence-thread-manager',
'javelin-behavior-detect-timezone',
'javelin-behavior-setup-check-https',
'javelin-behavior-aphlict-status',
'javelin-behavior-user-menu',
'phabricator-favicon',
),
'differential.pkg.css' => array(
'differential-core-view-css',
'differential-changeset-view-css',
'differential-revision-history-css',
'differential-revision-list-css',
'differential-table-of-contents-css',
'differential-revision-comment-css',
'differential-revision-add-comment-css',
'phabricator-object-selector-css',
'phabricator-content-source-view-css',
'inline-comment-summary-css',
'phui-inline-comment-view-css',
'phabricator-filetree-view-css',
),
'differential.pkg.js' => array(
'phabricator-drag-and-drop-file-upload',
'phabricator-shaped-request',
'javelin-behavior-differential-populate',
'javelin-behavior-differential-diff-radios',
'javelin-behavior-aphront-drag-and-drop-textarea',
'javelin-behavior-phabricator-object-selector',
'javelin-behavior-repository-crossreference',
- 'javelin-behavior-differential-user-select',
'javelin-behavior-aphront-more',
'phabricator-diff-inline',
'phabricator-diff-changeset',
'phabricator-diff-changeset-list',
),
'diffusion.pkg.css' => array(
'diffusion-icons-css',
),
'diffusion.pkg.js' => array(
'javelin-behavior-diffusion-pull-lastmodified',
'javelin-behavior-diffusion-commit-graph',
'javelin-behavior-audit-preview',
),
'maniphest.pkg.css' => array(
'maniphest-task-summary-css',
),
'maniphest.pkg.js' => array(
'javelin-behavior-maniphest-batch-selector',
- 'javelin-behavior-maniphest-subpriority-editor',
'javelin-behavior-maniphest-list-editor',
),
),
);
diff --git a/resources/celerity/packages.php b/resources/celerity/packages.php
index 4005e064b..6dbb66228 100644
--- a/resources/celerity/packages.php
+++ b/resources/celerity/packages.php
@@ -1,225 +1,223 @@
<?php
return array(
'core.pkg.js' => array(
'javelin-util',
'javelin-install',
'javelin-event',
'javelin-stratcom',
'javelin-behavior',
'javelin-resource',
'javelin-request',
'javelin-vector',
'javelin-dom',
'javelin-json',
'javelin-uri',
'javelin-workflow',
'javelin-mask',
'javelin-typeahead',
'javelin-typeahead-normalizer',
'javelin-typeahead-source',
'javelin-typeahead-preloaded-source',
'javelin-typeahead-ondemand-source',
'javelin-tokenizer',
'javelin-history',
'javelin-router',
'javelin-routable',
'javelin-behavior-aphront-basic-tokenizer',
'javelin-behavior-workflow',
'javelin-behavior-aphront-form-disable-on-submit',
'phabricator-keyboard-shortcut-manager',
'phabricator-keyboard-shortcut',
'javelin-behavior-phabricator-keyboard-shortcuts',
'javelin-behavior-refresh-csrf',
'javelin-behavior-phabricator-watch-anchor',
'javelin-behavior-phabricator-autofocus',
'phuix-dropdown-menu',
'phuix-action-list-view',
'phuix-action-view',
'phuix-icon-view',
'phabricator-phtize',
'javelin-behavior-phabricator-oncopy',
'phabricator-tooltip',
'javelin-behavior-phabricator-tooltips',
'phabricator-prefab',
'javelin-behavior-device',
'javelin-behavior-toggle-class',
'javelin-behavior-lightbox-attachments',
'phabricator-busy',
'javelin-sound',
'javelin-aphlict',
'phabricator-notification',
'javelin-behavior-aphlict-listen',
'javelin-behavior-phabricator-search-typeahead',
'javelin-behavior-aphlict-dropdown',
'javelin-behavior-history-install',
'javelin-behavior-phabricator-gesture',
'javelin-behavior-phabricator-active-nav',
'javelin-behavior-phabricator-nav',
'javelin-behavior-phabricator-remarkup-assist',
'phabricator-textareautils',
'phabricator-file-upload',
'javelin-behavior-global-drag-and-drop',
'javelin-behavior-phabricator-reveal-content',
'phui-hovercard',
'javelin-behavior-phui-hovercards',
'javelin-color',
'javelin-fx',
'phabricator-draggable-list',
'javelin-behavior-phabricator-transaction-list',
'javelin-behavior-phabricator-show-older-transactions',
'javelin-behavior-phui-dropdown-menu',
'javelin-behavior-doorkeeper-tag',
'phabricator-title',
'javelin-leader',
'javelin-websocket',
'javelin-behavior-dashboard-async-panel',
'javelin-behavior-dashboard-tab-panel',
'javelin-quicksand',
'javelin-behavior-quicksand-blacklist',
'javelin-behavior-high-security-warning',
'javelin-behavior-read-only-warning',
'javelin-scrollbar',
'javelin-behavior-scrollbar',
'javelin-behavior-durable-column',
'conpherence-thread-manager',
'javelin-behavior-detect-timezone',
'javelin-behavior-setup-check-https',
'javelin-behavior-aphlict-status',
'javelin-behavior-user-menu',
'phabricator-favicon',
),
'core.pkg.css' => array(
'phabricator-core-css',
'phabricator-zindex-css',
'phui-button-css',
'phui-button-simple-css',
'phui-theme-css',
'phabricator-standard-page-view',
'aphront-dialog-view-css',
'phui-form-view-css',
'aphront-panel-view-css',
'aphront-table-view-css',
'aphront-tokenizer-control-css',
'aphront-typeahead-control-css',
'aphront-list-filter-view-css',
'application-search-view-css',
'phabricator-remarkup-css',
'syntax-highlighting-css',
'syntax-default-css',
'phui-pager-css',
'aphront-tooltip-css',
'phabricator-flag-css',
'phui-info-view-css',
'phabricator-main-menu-view',
'phabricator-notification-css',
'phabricator-notification-menu-css',
'phui-lightbox-css',
'phui-comment-panel-css',
'phui-header-view-css',
'phabricator-nav-view-css',
'phui-basic-nav-view-css',
'phui-crumbs-view-css',
'phui-oi-list-view-css',
'phui-oi-color-css',
'phui-oi-big-ui-css',
'phui-oi-drag-ui-css',
'phui-oi-simple-ui-css',
'phui-oi-flush-ui-css',
'global-drag-and-drop-css',
'phui-spacing-css',
'phui-form-css',
'phui-icon-view-css',
'phabricator-action-list-view-css',
'phui-property-list-view-css',
'phui-tag-view-css',
'phui-list-view-css',
'font-fontawesome',
'font-lato',
'phui-font-icon-base-css',
'phui-fontkit-css',
'phui-box-css',
'phui-object-box-css',
'phui-timeline-view-css',
'phui-two-column-view-css',
'phui-curtain-view-css',
'sprite-login-css',
'sprite-tokens-css',
'tokens-css',
'auth-css',
'phui-status-list-view-css',
'phui-feed-story-css',
'phabricator-feed-css',
'phabricator-dashboard-css',
'aphront-multi-column-view-css',
),
'conpherence.pkg.css' => array(
'conpherence-durable-column-view',
'conpherence-menu-css',
'conpherence-color-css',
'conpherence-message-pane-css',
'conpherence-notification-css',
'conpherence-transaction-css',
'conpherence-participant-pane-css',
'conpherence-header-pane-css',
),
'conpherence.pkg.js' => array(
'javelin-behavior-conpherence-menu',
'javelin-behavior-conpherence-participant-pane',
'javelin-behavior-conpherence-pontificate',
'javelin-behavior-toggle-widget',
),
'differential.pkg.css' => array(
'differential-core-view-css',
'differential-changeset-view-css',
'differential-revision-history-css',
'differential-revision-list-css',
'differential-table-of-contents-css',
'differential-revision-comment-css',
'differential-revision-add-comment-css',
'phabricator-object-selector-css',
'phabricator-content-source-view-css',
'inline-comment-summary-css',
'phui-inline-comment-view-css',
'phabricator-filetree-view-css',
),
'differential.pkg.js' => array(
'phabricator-drag-and-drop-file-upload',
'phabricator-shaped-request',
'javelin-behavior-differential-populate',
'javelin-behavior-differential-diff-radios',
'javelin-behavior-aphront-drag-and-drop-textarea',
'javelin-behavior-phabricator-object-selector',
'javelin-behavior-repository-crossreference',
- 'javelin-behavior-differential-user-select',
'javelin-behavior-aphront-more',
'phabricator-diff-inline',
'phabricator-diff-changeset',
'phabricator-diff-changeset-list',
),
'diffusion.pkg.css' => array(
'diffusion-icons-css',
),
'diffusion.pkg.js' => array(
'javelin-behavior-diffusion-pull-lastmodified',
'javelin-behavior-diffusion-commit-graph',
'javelin-behavior-audit-preview',
),
'maniphest.pkg.css' => array(
'maniphest-task-summary-css',
),
'maniphest.pkg.js' => array(
'javelin-behavior-maniphest-batch-selector',
- 'javelin-behavior-maniphest-subpriority-editor',
'javelin-behavior-maniphest-list-editor',
),
);
diff --git a/resources/sql/autopatches/20151221.search.3.reindex.php b/resources/sql/autopatches/20151221.search.3.reindex.php
index 09556d5ea..623ba7bf6 100644
--- a/resources/sql/autopatches/20151221.search.3.reindex.php
+++ b/resources/sql/autopatches/20151221.search.3.reindex.php
@@ -1,11 +1,3 @@
<?php
-$table = new PhabricatorOwnersPackage();
-
-foreach (new LiskMigrationIterator($table) as $package) {
- PhabricatorSearchWorker::queueDocumentForIndexing(
- $package->getPHID(),
- array(
- 'force' => true,
- ));
-}
+// This was an old reindexing migration that has been obsoleted. See T13253.
diff --git a/resources/sql/autopatches/20160221.almanac.2.devicei.php b/resources/sql/autopatches/20160221.almanac.2.devicei.php
index aea17d0ad..623ba7bf6 100644
--- a/resources/sql/autopatches/20160221.almanac.2.devicei.php
+++ b/resources/sql/autopatches/20160221.almanac.2.devicei.php
@@ -1,11 +1,3 @@
<?php
-$table = new AlmanacDevice();
-
-foreach (new LiskMigrationIterator($table) as $device) {
- PhabricatorSearchWorker::queueDocumentForIndexing(
- $device->getPHID(),
- array(
- 'force' => true,
- ));
-}
+// This was an old reindexing migration that has been obsoleted. See T13253.
diff --git a/resources/sql/autopatches/20160221.almanac.4.servicei.php b/resources/sql/autopatches/20160221.almanac.4.servicei.php
index 97211ca7b..623ba7bf6 100644
--- a/resources/sql/autopatches/20160221.almanac.4.servicei.php
+++ b/resources/sql/autopatches/20160221.almanac.4.servicei.php
@@ -1,11 +1,3 @@
<?php
-$table = new AlmanacService();
-
-foreach (new LiskMigrationIterator($table) as $service) {
- PhabricatorSearchWorker::queueDocumentForIndexing(
- $service->getPHID(),
- array(
- 'force' => true,
- ));
-}
+// This was an old reindexing migration that has been obsoleted. See T13253.
diff --git a/resources/sql/autopatches/20160221.almanac.6.networki.php b/resources/sql/autopatches/20160221.almanac.6.networki.php
index 263defbb3..623ba7bf6 100644
--- a/resources/sql/autopatches/20160221.almanac.6.networki.php
+++ b/resources/sql/autopatches/20160221.almanac.6.networki.php
@@ -1,11 +1,3 @@
<?php
-$table = new AlmanacNetwork();
-
-foreach (new LiskMigrationIterator($table) as $network) {
- PhabricatorSearchWorker::queueDocumentForIndexing(
- $network->getPHID(),
- array(
- 'force' => true,
- ));
-}
+// This was an old reindexing migration that has been obsoleted. See T13253.
diff --git a/resources/sql/autopatches/20160227.harbormaster.2.plani.php b/resources/sql/autopatches/20160227.harbormaster.2.plani.php
index 6dea004c0..623ba7bf6 100644
--- a/resources/sql/autopatches/20160227.harbormaster.2.plani.php
+++ b/resources/sql/autopatches/20160227.harbormaster.2.plani.php
@@ -1,11 +1,3 @@
<?php
-$table = new HarbormasterBuildPlan();
-
-foreach (new LiskMigrationIterator($table) as $plan) {
- PhabricatorSearchWorker::queueDocumentForIndexing(
- $plan->getPHID(),
- array(
- 'force' => true,
- ));
-}
+// This was an old reindexing migration that has been obsoleted. See T13253.
diff --git a/resources/sql/autopatches/20160303.drydock.2.bluei.php b/resources/sql/autopatches/20160303.drydock.2.bluei.php
index c0b68c226..623ba7bf6 100644
--- a/resources/sql/autopatches/20160303.drydock.2.bluei.php
+++ b/resources/sql/autopatches/20160303.drydock.2.bluei.php
@@ -1,11 +1,3 @@
<?php
-$table = new DrydockBlueprint();
-
-foreach (new LiskMigrationIterator($table) as $blueprint) {
- PhabricatorSearchWorker::queueDocumentForIndexing(
- $blueprint->getPHID(),
- array(
- 'force' => true,
- ));
-}
+// This was an old reindexing migration that has been obsoleted. See T13253.
diff --git a/resources/sql/autopatches/20160308.nuance.04.sourcei.php b/resources/sql/autopatches/20160308.nuance.04.sourcei.php
index eb0d1da11..623ba7bf6 100644
--- a/resources/sql/autopatches/20160308.nuance.04.sourcei.php
+++ b/resources/sql/autopatches/20160308.nuance.04.sourcei.php
@@ -1,11 +1,3 @@
<?php
-$table = new NuanceSource();
-
-foreach (new LiskMigrationIterator($table) as $source) {
- PhabricatorSearchWorker::queueDocumentForIndexing(
- $source->getPHID(),
- array(
- 'force' => true,
- ));
-}
+// This was an old reindexing migration that has been obsoleted. See T13253.
diff --git a/resources/sql/autopatches/20160406.badges.ngrams.php b/resources/sql/autopatches/20160406.badges.ngrams.php
index ce8d8896e..623ba7bf6 100644
--- a/resources/sql/autopatches/20160406.badges.ngrams.php
+++ b/resources/sql/autopatches/20160406.badges.ngrams.php
@@ -1,11 +1,3 @@
<?php
-$table = new PhabricatorBadgesBadge();
-
-foreach (new LiskMigrationIterator($table) as $badge) {
- PhabricatorSearchWorker::queueDocumentForIndexing(
- $badge->getPHID(),
- array(
- 'force' => true,
- ));
-}
+// This was an old reindexing migration that has been obsoleted. See T13253.
diff --git a/resources/sql/autopatches/20160927.phurl.ngrams.php b/resources/sql/autopatches/20160927.phurl.ngrams.php
index 74cf61efa..623ba7bf6 100644
--- a/resources/sql/autopatches/20160927.phurl.ngrams.php
+++ b/resources/sql/autopatches/20160927.phurl.ngrams.php
@@ -1,11 +1,3 @@
<?php
-$table = new PhabricatorPhurlURL();
-
-foreach (new LiskMigrationIterator($table) as $url) {
- PhabricatorSearchWorker::queueDocumentForIndexing(
- $url->getPHID(),
- array(
- 'force' => true,
- ));
-}
+// This was an old reindexing migration that has been obsoleted. See T13253.
diff --git a/resources/sql/autopatches/20161011.conpherence.ngrams.php b/resources/sql/autopatches/20161011.conpherence.ngrams.php
index 457143f6c..623ba7bf6 100644
--- a/resources/sql/autopatches/20161011.conpherence.ngrams.php
+++ b/resources/sql/autopatches/20161011.conpherence.ngrams.php
@@ -1,11 +1,3 @@
<?php
-$table = new ConpherenceThread();
-
-foreach (new LiskMigrationIterator($table) as $thread) {
- PhabricatorSearchWorker::queueDocumentForIndexing(
- $thread->getPHID(),
- array(
- 'force' => true,
- ));
-}
+// This was an old reindexing migration that has been obsoleted. See T13253.
diff --git a/resources/sql/autopatches/20161216.dashboard.ngram.02.php b/resources/sql/autopatches/20161216.dashboard.ngram.02.php
index a7abc99b2..623ba7bf6 100644
--- a/resources/sql/autopatches/20161216.dashboard.ngram.02.php
+++ b/resources/sql/autopatches/20161216.dashboard.ngram.02.php
@@ -1,21 +1,3 @@
<?php
-$table_db = new PhabricatorDashboard();
-
-foreach (new LiskMigrationIterator($table_db) as $dashboard) {
- PhabricatorSearchWorker::queueDocumentForIndexing(
- $dashboard->getPHID(),
- array(
- 'force' => true,
- ));
-}
-
-$table_dbp = new PhabricatorDashboardPanel();
-
-foreach (new LiskMigrationIterator($table_dbp) as $panel) {
- PhabricatorSearchWorker::queueDocumentForIndexing(
- $panel->getPHID(),
- array(
- 'force' => true,
- ));
-}
+// This was an old reindexing migration that has been obsoleted. See T13253.
diff --git a/resources/sql/autopatches/20170526.milestones.php b/resources/sql/autopatches/20170526.milestones.php
index 2e30ac477..623ba7bf6 100644
--- a/resources/sql/autopatches/20170526.milestones.php
+++ b/resources/sql/autopatches/20170526.milestones.php
@@ -1,11 +1,3 @@
<?php
-$table = new PhabricatorProject();
-
-foreach (new LiskMigrationIterator($table) as $project) {
- PhabricatorSearchWorker::queueDocumentForIndexing(
- $project->getPHID(),
- array(
- 'force' => true,
- ));
-}
+// This was an old reindexing migration that has been obsoleted. See T13253.
diff --git a/resources/sql/autopatches/20171026.ferret.05.ponder.index.php b/resources/sql/autopatches/20171026.ferret.05.ponder.index.php
index 20489846d..623ba7bf6 100644
--- a/resources/sql/autopatches/20171026.ferret.05.ponder.index.php
+++ b/resources/sql/autopatches/20171026.ferret.05.ponder.index.php
@@ -1,11 +1,3 @@
<?php
-$table = new PonderQuestion();
-
-foreach (new LiskMigrationIterator($table) as $question) {
- PhabricatorSearchWorker::queueDocumentForIndexing(
- $question->getPHID(),
- array(
- 'force' => true,
- ));
-}
+// This was an old reindexing migration that has been obsoleted. See T13253.
diff --git a/resources/sql/autopatches/20190206.external.01.legalpad.sql b/resources/sql/autopatches/20190206.external.01.legalpad.sql
new file mode 100644
index 000000000..8afa9dd9f
--- /dev/null
+++ b/resources/sql/autopatches/20190206.external.01.legalpad.sql
@@ -0,0 +1,2 @@
+UPDATE {$NAMESPACE}_legalpad.legalpad_documentsignature
+ SET signerPHID = NULL WHERE signerPHID LIKE 'PHID-XUSR-%';
diff --git a/resources/sql/autopatches/20190206.external.02.email.sql b/resources/sql/autopatches/20190206.external.02.email.sql
new file mode 100644
index 000000000..14f5f4791
--- /dev/null
+++ b/resources/sql/autopatches/20190206.external.02.email.sql
@@ -0,0 +1,2 @@
+DELETE FROM {$NAMESPACE}_user.user_externalaccount
+ WHERE accountType = 'email';
diff --git a/resources/sql/autopatches/20190206.external.03.providerphid.sql b/resources/sql/autopatches/20190206.external.03.providerphid.sql
new file mode 100644
index 000000000..0b2f498e0
--- /dev/null
+++ b/resources/sql/autopatches/20190206.external.03.providerphid.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_user.user_externalaccount
+ ADD providerConfigPHID VARBINARY(64) NOT NULL;
diff --git a/resources/sql/autopatches/20190206.external.04.providerlink.php b/resources/sql/autopatches/20190206.external.04.providerlink.php
new file mode 100644
index 000000000..e4a2e2d4b
--- /dev/null
+++ b/resources/sql/autopatches/20190206.external.04.providerlink.php
@@ -0,0 +1,36 @@
+<?php
+
+$account_table = new PhabricatorExternalAccount();
+$account_conn = $account_table->establishConnection('w');
+$table_name = $account_table->getTableName();
+
+$config_table = new PhabricatorAuthProviderConfig();
+$config_conn = $config_table->establishConnection('w');
+
+foreach (new LiskRawMigrationIterator($account_conn, $table_name) as $row) {
+ if (strlen($row['providerConfigPHID'])) {
+ continue;
+ }
+
+ $config_row = queryfx_one(
+ $config_conn,
+ 'SELECT phid
+ FROM %R
+ WHERE providerType = %s AND providerDomain = %s
+ LIMIT 1',
+ $config_table,
+ $row['accountType'],
+ $row['accountDomain']);
+ if (!$config_row) {
+ continue;
+ }
+
+ queryfx(
+ $account_conn,
+ 'UPDATE %R
+ SET providerConfigPHID = %s
+ WHERE id = %d',
+ $account_table,
+ $config_row['phid'],
+ $row['id']);
+}
diff --git a/resources/sql/autopatches/20190207.packages.01.state.sql b/resources/sql/autopatches/20190207.packages.01.state.sql
new file mode 100644
index 000000000..0e74f269b
--- /dev/null
+++ b/resources/sql/autopatches/20190207.packages.01.state.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_owners.owners_package
+ ADD auditingState VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20190207.packages.02.migrate.sql b/resources/sql/autopatches/20190207.packages.02.migrate.sql
new file mode 100644
index 000000000..60bf364ac
--- /dev/null
+++ b/resources/sql/autopatches/20190207.packages.02.migrate.sql
@@ -0,0 +1,2 @@
+UPDATE {$NAMESPACE}_owners.owners_package
+ SET auditingState = IF(auditingEnabled = 0, 'none', 'audit');
diff --git a/resources/sql/autopatches/20190207.packages.03.drop.sql b/resources/sql/autopatches/20190207.packages.03.drop.sql
new file mode 100644
index 000000000..24d0ce1a4
--- /dev/null
+++ b/resources/sql/autopatches/20190207.packages.03.drop.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_owners.owners_package
+ DROP auditingEnabled;
diff --git a/resources/sql/autopatches/20190207.packages.04.xactions.php b/resources/sql/autopatches/20190207.packages.04.xactions.php
new file mode 100644
index 000000000..5a8609166
--- /dev/null
+++ b/resources/sql/autopatches/20190207.packages.04.xactions.php
@@ -0,0 +1,41 @@
+<?php
+
+$table = new PhabricatorOwnersPackageTransaction();
+$conn = $table->establishConnection('w');
+$iterator = new LiskRawMigrationIterator($conn, $table->getTableName());
+
+// Migrate "Auditing State" transactions for Owners Packages from old values
+// (which were "0" or "1", as JSON integer literals, without quotes) to new
+// values (which are JSON strings, with quotes).
+
+foreach ($iterator as $row) {
+ if ($row['transactionType'] !== 'owners.auditing') {
+ continue;
+ }
+
+ $old_value = (int)$row['oldValue'];
+ $new_value = (int)$row['newValue'];
+
+ if (!$old_value) {
+ $old_value = 'none';
+ } else {
+ $old_value = 'audit';
+ }
+
+ if (!$new_value) {
+ $new_value = 'none';
+ } else {
+ $new_value = 'audit';
+ }
+
+ $old_value = phutil_json_encode($old_value);
+ $new_value = phutil_json_encode($new_value);
+
+ queryfx(
+ $conn,
+ 'UPDATE %R SET oldValue = %s, newValue = %s WHERE id = %d',
+ $table,
+ $old_value,
+ $new_value,
+ $row['id']);
+}
diff --git a/resources/sql/autopatches/20190215.daemons.01.dropdataid.php b/resources/sql/autopatches/20190215.daemons.01.dropdataid.php
new file mode 100644
index 000000000..05cc4adfe
--- /dev/null
+++ b/resources/sql/autopatches/20190215.daemons.01.dropdataid.php
@@ -0,0 +1,21 @@
+<?php
+
+// See T6615. We're about to change the nullability on the "dataID" column,
+// but it may have a UNIQUE KEY on it. Make sure we get rid of this key first
+// so we don't run into trouble.
+
+// There's no "IF EXISTS" modifier for "ALTER TABLE" so run this as a PHP patch
+// instead of an SQL patch.
+
+$table = new PhabricatorWorkerActiveTask();
+$conn = $table->establishConnection('w');
+
+try {
+ queryfx(
+ $conn,
+ 'ALTER TABLE %R DROP KEY %T',
+ $table,
+ 'dataID');
+} catch (AphrontQueryException $ex) {
+ // Ignore.
+}
diff --git a/resources/sql/autopatches/20190215.daemons.02.nulldataid.sql b/resources/sql/autopatches/20190215.daemons.02.nulldataid.sql
new file mode 100644
index 000000000..19be602ef
--- /dev/null
+++ b/resources/sql/autopatches/20190215.daemons.02.nulldataid.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_worker.worker_activetask
+ CHANGE dataID dataID INT UNSIGNED NOT NULL;
diff --git a/resources/sql/autopatches/20190215.harbor.01.stringindex.sql b/resources/sql/autopatches/20190215.harbor.01.stringindex.sql
new file mode 100644
index 000000000..e94b240ba
--- /dev/null
+++ b/resources/sql/autopatches/20190215.harbor.01.stringindex.sql
@@ -0,0 +1,6 @@
+CREATE TABLE {$NAMESPACE}_harbormaster.harbormaster_string (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ stringIndex BINARY(12) NOT NULL,
+ stringValue LONGTEXT NOT NULL,
+ UNIQUE KEY `key_string` (stringIndex)
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20190215.harbor.02.stringcol.sql b/resources/sql/autopatches/20190215.harbor.02.stringcol.sql
new file mode 100644
index 000000000..acfdb0f86
--- /dev/null
+++ b/resources/sql/autopatches/20190215.harbor.02.stringcol.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildunitmessage
+ ADD nameIndex BINARY(12) NOT NULL;
diff --git a/resources/sql/autopatches/20190220.daemon_worker.completed.01.sql b/resources/sql/autopatches/20190220.daemon_worker.completed.01.sql
new file mode 100644
index 000000000..37f5a89bb
--- /dev/null
+++ b/resources/sql/autopatches/20190220.daemon_worker.completed.01.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_worker.worker_archivetask
+ ADD archivedEpoch INT UNSIGNED NULL;
diff --git a/resources/sql/autopatches/20190220.daemon_worker.completed.02.sql b/resources/sql/autopatches/20190220.daemon_worker.completed.02.sql
new file mode 100644
index 000000000..f0040576a
--- /dev/null
+++ b/resources/sql/autopatches/20190220.daemon_worker.completed.02.sql
@@ -0,0 +1,3 @@
+ALTER TABLE {$NAMESPACE}_worker.worker_activetask
+ ADD dateCreated int unsigned NOT NULL,
+ ADD dateModified int unsigned NOT NULL;
diff --git a/resources/sql/autopatches/20190226.harbor.01.planprops.sql b/resources/sql/autopatches/20190226.harbor.01.planprops.sql
new file mode 100644
index 000000000..324139669
--- /dev/null
+++ b/resources/sql/autopatches/20190226.harbor.01.planprops.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildplan
+ ADD properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20190226.harbor.02.planvalue.sql b/resources/sql/autopatches/20190226.harbor.02.planvalue.sql
new file mode 100644
index 000000000..b1929abf5
--- /dev/null
+++ b/resources/sql/autopatches/20190226.harbor.02.planvalue.sql
@@ -0,0 +1,2 @@
+UPDATE {$NAMESPACE}_harbormaster.harbormaster_buildplan
+ SET properties = '{}' WHERE properties = '';
diff --git a/resources/sql/autopatches/20190307.herald.01.comments.sql b/resources/sql/autopatches/20190307.herald.01.comments.sql
new file mode 100644
index 000000000..ff9cb9af8
--- /dev/null
+++ b/resources/sql/autopatches/20190307.herald.01.comments.sql
@@ -0,0 +1 @@
+DROP TABLE {$NAMESPACE}_herald.herald_ruletransaction_comment;
diff --git a/resources/sql/autopatches/20190312.triggers.01.trigger.sql b/resources/sql/autopatches/20190312.triggers.01.trigger.sql
new file mode 100644
index 000000000..301a3a62c
--- /dev/null
+++ b/resources/sql/autopatches/20190312.triggers.01.trigger.sql
@@ -0,0 +1,9 @@
+CREATE TABLE {$NAMESPACE}_project.project_trigger (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ phid VARBINARY(64) NOT NULL,
+ name VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT},
+ editPolicy VARBINARY(64) NOT NULL,
+ ruleset LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20190312.triggers.02.xaction.sql b/resources/sql/autopatches/20190312.triggers.02.xaction.sql
new file mode 100644
index 000000000..1a6034c4b
--- /dev/null
+++ b/resources/sql/autopatches/20190312.triggers.02.xaction.sql
@@ -0,0 +1,19 @@
+CREATE TABLE {$NAMESPACE}_project.project_triggertransaction (
+ 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) NOT NULL,
+ oldValue LONGTEXT NOT NULL,
+ newValue LONGTEXT NOT NULL,
+ contentSource LONGTEXT NOT NULL,
+ metadata LONGTEXT NOT NULL,
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL,
+ UNIQUE KEY `key_phid` (`phid`),
+ KEY `key_object` (`objectPHID`)
+) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20190312.triggers.03.triggerphid.sql b/resources/sql/autopatches/20190312.triggers.03.triggerphid.sql
new file mode 100644
index 000000000..271d679cf
--- /dev/null
+++ b/resources/sql/autopatches/20190312.triggers.03.triggerphid.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_project.project_column
+ ADD triggerPHID VARBINARY(64);
diff --git a/resources/sql/autopatches/20190322.triggers.01.usage.sql b/resources/sql/autopatches/20190322.triggers.01.usage.sql
new file mode 100644
index 000000000..643ebbbff
--- /dev/null
+++ b/resources/sql/autopatches/20190322.triggers.01.usage.sql
@@ -0,0 +1,8 @@
+CREATE TABLE {$NAMESPACE}_project.project_triggerusage (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ triggerPHID VARBINARY(64) NOT NULL,
+ examplePHID VARBINARY(64),
+ columnCount INT UNSIGNED NOT NULL,
+ activeColumnCount INT UNSIGNED NOT NULL,
+ UNIQUE KEY `key_trigger` (triggerPHID)
+) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/patches/133.imagemacro.sql b/resources/sql/patches/133.imagemacro.sql
index 01852c6b4..1477fd879 100644
--- a/resources/sql/patches/133.imagemacro.sql
+++ b/resources/sql/patches/133.imagemacro.sql
@@ -1,2 +1,2 @@
-ALTER IGNORE TABLE `{$NAMESPACE}_file`.`file_imagemacro`
- ADD UNIQUE `name` (`name`);
+ALTER TABLE `{$NAMESPACE}_file`.`file_imagemacro`
+ ADD UNIQUE KEY `name` (`name`);
diff --git a/resources/sql/patches/20130611.migrateoauth.php b/resources/sql/patches/20130611.migrateoauth.php
index 3622b2772..92fe854cf 100644
--- a/resources/sql/patches/20130611.migrateoauth.php
+++ b/resources/sql/patches/20130611.migrateoauth.php
@@ -1,66 +1,14 @@
<?php
-// NOTE: We aren't using PhabricatorUserOAuthInfo anywhere here because it is
-// getting nuked in a future diff.
-
$table = new PhabricatorUser();
+$conn = $table->establishConnection('w');
$table_name = 'user_oauthinfo';
-$conn_w = $table->establishConnection('w');
-
-$xaccount = new PhabricatorExternalAccount();
-
-echo pht('Migrating OAuth to %s...', 'ExternalAccount')."\n";
-$domain_map = array(
- 'disqus' => 'disqus.com',
- 'facebook' => 'facebook.com',
- 'github' => 'github.com',
- 'google' => 'google.com',
-);
-
-try {
- $phabricator_oauth_uri = new PhutilURI(
- PhabricatorEnv::getEnvConfig('phabricator.oauth-uri'));
- $domain_map['phabricator'] = $phabricator_oauth_uri->getDomain();
-} catch (Exception $ex) {
- // Ignore; this likely indicates that we have removed `phabricator.oauth-uri`
- // in some future diff.
+foreach (new LiskRawMigrationIterator($conn, $table_name) as $row) {
+ throw new Exception(
+ pht(
+ 'Your Phabricator install has ancient OAuth account data and is '.
+ 'too old to upgrade directly to a modern version of Phabricator. '.
+ 'Upgrade to a version released between June 2013 and February 2019 '.
+ 'first, then upgrade to a modern version.'));
}
-
-$rows = queryfx_all(
- $conn_w,
- 'SELECT * FROM user_oauthinfo');
-foreach ($rows as $row) {
- echo pht('Migrating row ID #%d.', $row['id'])."\n";
- $user = id(new PhabricatorUser())->loadOneWhere(
- 'id = %d',
- $row['userID']);
- if (!$user) {
- echo pht('Bad user ID!')."\n";
- continue;
- }
-
- $domain = idx($domain_map, $row['oauthProvider']);
- if (empty($domain)) {
- echo pht('Unknown OAuth provider!')."\n";
- continue;
- }
-
-
- $xaccount = id(new PhabricatorExternalAccount())
- ->setUserPHID($user->getPHID())
- ->setAccountType($row['oauthProvider'])
- ->setAccountDomain($domain)
- ->setAccountID($row['oauthUID'])
- ->setAccountURI($row['accountURI'])
- ->setUsername($row['accountName'])
- ->setDateCreated($row['dateCreated']);
-
- try {
- $xaccount->save();
- } catch (Exception $ex) {
- phlog($ex);
- }
-}
-
-echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20130611.nukeldap.php b/resources/sql/patches/20130611.nukeldap.php
index 3f225cfa8..0f0b976a5 100644
--- a/resources/sql/patches/20130611.nukeldap.php
+++ b/resources/sql/patches/20130611.nukeldap.php
@@ -1,41 +1,14 @@
<?php
-// NOTE: We aren't using PhabricatorUserLDAPInfo anywhere here because it is
-// being nuked by this change
-
$table = new PhabricatorUser();
+$conn = $table->establishConnection('w');
$table_name = 'user_ldapinfo';
-$conn_w = $table->establishConnection('w');
-
-$xaccount = new PhabricatorExternalAccount();
-
-echo pht('Migrating LDAP to %s...', 'ExternalAccount')."\n";
-
-$rows = queryfx_all($conn_w, 'SELECT * FROM %T', $table_name);
-foreach ($rows as $row) {
- echo pht('Migrating row ID #%d.', $row['id'])."\n";
- $user = id(new PhabricatorUser())->loadOneWhere(
- 'id = %d',
- $row['userID']);
- if (!$user) {
- echo pht('Bad user ID!')."\n";
- continue;
- }
-
- $xaccount = id(new PhabricatorExternalAccount())
- ->setUserPHID($user->getPHID())
- ->setAccountType('ldap')
- ->setAccountDomain('self')
- ->setAccountID($row['ldapUsername'])
- ->setUsername($row['ldapUsername'])
- ->setDateCreated($row['dateCreated']);
-
- try {
- $xaccount->save();
- } catch (Exception $ex) {
- phlog($ex);
- }
+foreach (new LiskRawMigrationIterator($conn, $table_name) as $row) {
+ throw new Exception(
+ pht(
+ 'Your Phabricator install has ancient LDAP account data and is '.
+ 'too old to upgrade directly to a modern version of Phabricator. '.
+ 'Upgrade to a version released between June 2013 and February 2019 '.
+ 'first, then upgrade to a modern version.'));
}
-
-echo pht('Done.')."\n";
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 324fc3c5d..33f2cf4d3 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,11722 +1,11890 @@
<?php
/**
* This file is automatically generated. Use 'arc liberate' to rebuild it.
*
* @generated
* @phutil-library-version 2
*/
phutil_register_library_map(array(
'__library_version__' => 2,
'class' => array(
'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',
'AlmanacClusterDatabaseServiceType' => 'applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php',
'AlmanacClusterRepositoryServiceType' => 'applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php',
'AlmanacClusterServiceType' => 'applications/almanac/servicetype/AlmanacClusterServiceType.php',
'AlmanacConsoleController' => 'applications/almanac/controller/AlmanacConsoleController.php',
'AlmanacController' => 'applications/almanac/controller/AlmanacController.php',
'AlmanacCreateDevicesCapability' => 'applications/almanac/capability/AlmanacCreateDevicesCapability.php',
'AlmanacCreateNamespacesCapability' => 'applications/almanac/capability/AlmanacCreateNamespacesCapability.php',
'AlmanacCreateNetworksCapability' => 'applications/almanac/capability/AlmanacCreateNetworksCapability.php',
'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',
'AlmanacPropertyDeleteController' => 'applications/almanac/controller/AlmanacPropertyDeleteController.php',
'AlmanacPropertyEditController' => 'applications/almanac/controller/AlmanacPropertyEditController.php',
'AlmanacPropertyEditEngine' => 'applications/almanac/editor/AlmanacPropertyEditEngine.php',
'AlmanacPropertyInterface' => 'applications/almanac/property/AlmanacPropertyInterface.php',
'AlmanacPropertyQuery' => 'applications/almanac/query/AlmanacPropertyQuery.php',
'AlmanacQuery' => 'applications/almanac/query/AlmanacQuery.php',
'AlmanacSchemaSpec' => 'applications/almanac/storage/AlmanacSchemaSpec.php',
'AlmanacSearchEngineAttachment' => 'applications/almanac/engineextension/AlmanacSearchEngineAttachment.php',
'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',
'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',
'Aphront403Response' => 'aphront/response/Aphront403Response.php',
'Aphront404Response' => 'aphront/response/Aphront404Response.php',
'AphrontAjaxResponse' => 'aphront/response/AphrontAjaxResponse.php',
'AphrontApplicationConfiguration' => 'aphront/configuration/AphrontApplicationConfiguration.php',
'AphrontBarView' => 'view/widget/bars/AphrontBarView.php',
'AphrontBoolHTTPParameterType' => 'aphront/httpparametertype/AphrontBoolHTTPParameterType.php',
'AphrontCalendarEventView' => 'applications/calendar/view/AphrontCalendarEventView.php',
'AphrontController' => 'aphront/AphrontController.php',
'AphrontCursorPagerView' => 'view/control/AphrontCursorPagerView.php',
'AphrontDialogResponse' => 'aphront/response/AphrontDialogResponse.php',
'AphrontDialogView' => 'view/AphrontDialogView.php',
'AphrontEpochHTTPParameterType' => 'aphront/httpparametertype/AphrontEpochHTTPParameterType.php',
'AphrontException' => 'aphront/exception/AphrontException.php',
'AphrontFileHTTPParameterType' => 'aphront/httpparametertype/AphrontFileHTTPParameterType.php',
'AphrontFileResponse' => 'aphront/response/AphrontFileResponse.php',
'AphrontFormCheckboxControl' => 'view/form/control/AphrontFormCheckboxControl.php',
'AphrontFormControl' => 'view/form/control/AphrontFormControl.php',
'AphrontFormDateControl' => 'view/form/control/AphrontFormDateControl.php',
'AphrontFormDateControlValue' => 'view/form/control/AphrontFormDateControlValue.php',
'AphrontFormDividerControl' => 'view/form/control/AphrontFormDividerControl.php',
'AphrontFormFileControl' => 'view/form/control/AphrontFormFileControl.php',
'AphrontFormHandlesControl' => 'view/form/control/AphrontFormHandlesControl.php',
'AphrontFormMarkupControl' => 'view/form/control/AphrontFormMarkupControl.php',
'AphrontFormPasswordControl' => 'view/form/control/AphrontFormPasswordControl.php',
'AphrontFormPolicyControl' => 'view/form/control/AphrontFormPolicyControl.php',
'AphrontFormRadioButtonControl' => 'view/form/control/AphrontFormRadioButtonControl.php',
'AphrontFormRecaptchaControl' => 'view/form/control/AphrontFormRecaptchaControl.php',
'AphrontFormSelectControl' => 'view/form/control/AphrontFormSelectControl.php',
'AphrontFormStaticControl' => 'view/form/control/AphrontFormStaticControl.php',
'AphrontFormSubmitControl' => 'view/form/control/AphrontFormSubmitControl.php',
'AphrontFormTextAreaControl' => 'view/form/control/AphrontFormTextAreaControl.php',
'AphrontFormTextControl' => 'view/form/control/AphrontFormTextControl.php',
'AphrontFormTextWithSubmitControl' => 'view/form/control/AphrontFormTextWithSubmitControl.php',
'AphrontFormTokenizerControl' => 'view/form/control/AphrontFormTokenizerControl.php',
'AphrontFormTypeaheadControl' => 'view/form/control/AphrontFormTypeaheadControl.php',
'AphrontFormView' => 'view/form/AphrontFormView.php',
'AphrontGlyphBarView' => 'view/widget/bars/AphrontGlyphBarView.php',
'AphrontHTMLResponse' => 'aphront/response/AphrontHTMLResponse.php',
'AphrontHTTPParameterType' => 'aphront/httpparametertype/AphrontHTTPParameterType.php',
'AphrontHTTPProxyResponse' => 'aphront/response/AphrontHTTPProxyResponse.php',
'AphrontHTTPSink' => 'aphront/sink/AphrontHTTPSink.php',
'AphrontHTTPSinkTestCase' => 'aphront/sink/__tests__/AphrontHTTPSinkTestCase.php',
'AphrontIntHTTPParameterType' => 'aphront/httpparametertype/AphrontIntHTTPParameterType.php',
'AphrontIsolatedDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontIsolatedDatabaseConnectionTestCase.php',
'AphrontIsolatedHTTPSink' => 'aphront/sink/AphrontIsolatedHTTPSink.php',
'AphrontJSONResponse' => 'aphront/response/AphrontJSONResponse.php',
'AphrontJavelinView' => 'view/AphrontJavelinView.php',
'AphrontKeyboardShortcutsAvailableView' => 'view/widget/AphrontKeyboardShortcutsAvailableView.php',
'AphrontListFilterView' => 'view/layout/AphrontListFilterView.php',
'AphrontListHTTPParameterType' => 'aphront/httpparametertype/AphrontListHTTPParameterType.php',
'AphrontMalformedRequestException' => 'aphront/exception/AphrontMalformedRequestException.php',
'AphrontMoreView' => 'view/layout/AphrontMoreView.php',
'AphrontMultiColumnView' => 'view/layout/AphrontMultiColumnView.php',
'AphrontMySQLDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php',
'AphrontNullView' => 'view/AphrontNullView.php',
'AphrontPHIDHTTPParameterType' => 'aphront/httpparametertype/AphrontPHIDHTTPParameterType.php',
'AphrontPHIDListHTTPParameterType' => 'aphront/httpparametertype/AphrontPHIDListHTTPParameterType.php',
'AphrontPHPHTTPSink' => 'aphront/sink/AphrontPHPHTTPSink.php',
'AphrontPageView' => 'view/page/AphrontPageView.php',
'AphrontPlainTextResponse' => 'aphront/response/AphrontPlainTextResponse.php',
'AphrontProgressBarView' => 'view/widget/bars/AphrontProgressBarView.php',
'AphrontProjectListHTTPParameterType' => 'aphront/httpparametertype/AphrontProjectListHTTPParameterType.php',
'AphrontProxyResponse' => 'aphront/response/AphrontProxyResponse.php',
'AphrontRedirectResponse' => 'aphront/response/AphrontRedirectResponse.php',
'AphrontRedirectResponseTestCase' => 'aphront/response/__tests__/AphrontRedirectResponseTestCase.php',
'AphrontReloadResponse' => 'aphront/response/AphrontReloadResponse.php',
'AphrontRequest' => 'aphront/AphrontRequest.php',
'AphrontRequestExceptionHandler' => 'aphront/handler/AphrontRequestExceptionHandler.php',
'AphrontRequestTestCase' => 'aphront/__tests__/AphrontRequestTestCase.php',
'AphrontResponse' => 'aphront/response/AphrontResponse.php',
'AphrontResponseProducerInterface' => 'aphront/interface/AphrontResponseProducerInterface.php',
'AphrontRoutingMap' => 'aphront/site/AphrontRoutingMap.php',
'AphrontRoutingResult' => 'aphront/site/AphrontRoutingResult.php',
'AphrontSelectHTTPParameterType' => 'aphront/httpparametertype/AphrontSelectHTTPParameterType.php',
'AphrontSideNavFilterView' => 'view/layout/AphrontSideNavFilterView.php',
'AphrontSite' => 'aphront/site/AphrontSite.php',
'AphrontStackTraceView' => 'view/widget/AphrontStackTraceView.php',
'AphrontStandaloneHTMLResponse' => 'aphront/response/AphrontStandaloneHTMLResponse.php',
'AphrontStringHTTPParameterType' => 'aphront/httpparametertype/AphrontStringHTTPParameterType.php',
'AphrontStringListHTTPParameterType' => 'aphront/httpparametertype/AphrontStringListHTTPParameterType.php',
'AphrontTableView' => 'view/control/AphrontTableView.php',
'AphrontTagView' => 'view/AphrontTagView.php',
'AphrontTokenizerTemplateView' => 'view/control/AphrontTokenizerTemplateView.php',
'AphrontTypeaheadTemplateView' => 'view/control/AphrontTypeaheadTemplateView.php',
'AphrontUnhandledExceptionResponse' => 'aphront/response/AphrontUnhandledExceptionResponse.php',
'AphrontUserListHTTPParameterType' => 'aphront/httpparametertype/AphrontUserListHTTPParameterType.php',
'AphrontView' => 'view/AphrontView.php',
'AphrontWebpageResponse' => 'aphront/response/AphrontWebpageResponse.php',
'ArcanistConduitAPIMethod' => 'applications/arcanist/conduit/ArcanistConduitAPIMethod.php',
'AuditConduitAPIMethod' => 'applications/audit/conduit/AuditConduitAPIMethod.php',
'AuditQueryConduitAPIMethod' => 'applications/audit/conduit/AuditQueryConduitAPIMethod.php',
'AuthManageProvidersCapability' => 'applications/auth/capability/AuthManageProvidersCapability.php',
'BulkParameterType' => 'applications/transactions/bulk/type/BulkParameterType.php',
'BulkPointsParameterType' => 'applications/transactions/bulk/type/BulkPointsParameterType.php',
'BulkRemarkupParameterType' => 'applications/transactions/bulk/type/BulkRemarkupParameterType.php',
'BulkSelectParameterType' => 'applications/transactions/bulk/type/BulkSelectParameterType.php',
'BulkStringParameterType' => 'applications/transactions/bulk/type/BulkStringParameterType.php',
'BulkTokenizerParameterType' => 'applications/transactions/bulk/type/BulkTokenizerParameterType.php',
'CalendarTimeUtil' => 'applications/calendar/util/CalendarTimeUtil.php',
'CalendarTimeUtilTestCase' => 'applications/calendar/__tests__/CalendarTimeUtilTestCase.php',
'CelerityAPI' => 'applications/celerity/CelerityAPI.php',
'CelerityDarkModePostprocessor' => 'applications/celerity/postprocessor/CelerityDarkModePostprocessor.php',
'CelerityDefaultPostprocessor' => 'applications/celerity/postprocessor/CelerityDefaultPostprocessor.php',
'CelerityHighContrastPostprocessor' => 'applications/celerity/postprocessor/CelerityHighContrastPostprocessor.php',
'CelerityLargeFontPostprocessor' => 'applications/celerity/postprocessor/CelerityLargeFontPostprocessor.php',
'CelerityManagementMapWorkflow' => 'applications/celerity/management/CelerityManagementMapWorkflow.php',
'CelerityManagementSyntaxWorkflow' => 'applications/celerity/management/CelerityManagementSyntaxWorkflow.php',
'CelerityManagementWorkflow' => 'applications/celerity/management/CelerityManagementWorkflow.php',
'CelerityPhabricatorResourceController' => 'applications/celerity/controller/CelerityPhabricatorResourceController.php',
'CelerityPhabricatorResources' => 'applications/celerity/resources/CelerityPhabricatorResources.php',
'CelerityPhysicalResources' => 'applications/celerity/resources/CelerityPhysicalResources.php',
'CelerityPhysicalResourcesTestCase' => 'applications/celerity/resources/__tests__/CelerityPhysicalResourcesTestCase.php',
'CelerityPostprocessor' => 'applications/celerity/postprocessor/CelerityPostprocessor.php',
'CelerityPostprocessorTestCase' => 'applications/celerity/__tests__/CelerityPostprocessorTestCase.php',
'CelerityRedGreenPostprocessor' => 'applications/celerity/postprocessor/CelerityRedGreenPostprocessor.php',
'CelerityResourceController' => 'applications/celerity/controller/CelerityResourceController.php',
'CelerityResourceGraph' => 'applications/celerity/CelerityResourceGraph.php',
'CelerityResourceMap' => 'applications/celerity/CelerityResourceMap.php',
'CelerityResourceMapGenerator' => 'applications/celerity/CelerityResourceMapGenerator.php',
'CelerityResourceTransformer' => 'applications/celerity/CelerityResourceTransformer.php',
'CelerityResourceTransformerTestCase' => 'applications/celerity/__tests__/CelerityResourceTransformerTestCase.php',
'CelerityResources' => 'applications/celerity/resources/CelerityResources.php',
'CelerityResourcesOnDisk' => 'applications/celerity/resources/CelerityResourcesOnDisk.php',
'CeleritySpriteGenerator' => 'applications/celerity/CeleritySpriteGenerator.php',
'CelerityStaticResourceResponse' => 'applications/celerity/CelerityStaticResourceResponse.php',
'ChatLogConduitAPIMethod' => 'applications/chatlog/conduit/ChatLogConduitAPIMethod.php',
'ChatLogQueryConduitAPIMethod' => 'applications/chatlog/conduit/ChatLogQueryConduitAPIMethod.php',
'ChatLogRecordConduitAPIMethod' => 'applications/chatlog/conduit/ChatLogRecordConduitAPIMethod.php',
'ConduitAPIMethod' => 'applications/conduit/method/ConduitAPIMethod.php',
'ConduitAPIMethodTestCase' => 'applications/conduit/method/__tests__/ConduitAPIMethodTestCase.php',
'ConduitAPIRequest' => 'applications/conduit/protocol/ConduitAPIRequest.php',
'ConduitAPIResponse' => 'applications/conduit/protocol/ConduitAPIResponse.php',
'ConduitApplicationNotInstalledException' => 'applications/conduit/protocol/exception/ConduitApplicationNotInstalledException.php',
'ConduitBoolParameterType' => 'applications/conduit/parametertype/ConduitBoolParameterType.php',
'ConduitCall' => 'applications/conduit/call/ConduitCall.php',
'ConduitCallTestCase' => 'applications/conduit/call/__tests__/ConduitCallTestCase.php',
'ConduitColumnsParameterType' => 'applications/conduit/parametertype/ConduitColumnsParameterType.php',
'ConduitConnectConduitAPIMethod' => 'applications/conduit/method/ConduitConnectConduitAPIMethod.php',
'ConduitConstantDescription' => 'applications/conduit/data/ConduitConstantDescription.php',
'ConduitEpochParameterType' => 'applications/conduit/parametertype/ConduitEpochParameterType.php',
'ConduitException' => 'applications/conduit/protocol/exception/ConduitException.php',
'ConduitGetCapabilitiesConduitAPIMethod' => 'applications/conduit/method/ConduitGetCapabilitiesConduitAPIMethod.php',
'ConduitGetCertificateConduitAPIMethod' => 'applications/conduit/method/ConduitGetCertificateConduitAPIMethod.php',
'ConduitIntListParameterType' => 'applications/conduit/parametertype/ConduitIntListParameterType.php',
'ConduitIntParameterType' => 'applications/conduit/parametertype/ConduitIntParameterType.php',
'ConduitListParameterType' => 'applications/conduit/parametertype/ConduitListParameterType.php',
'ConduitLogGarbageCollector' => 'applications/conduit/garbagecollector/ConduitLogGarbageCollector.php',
'ConduitMethodDoesNotExistException' => 'applications/conduit/protocol/exception/ConduitMethodDoesNotExistException.php',
'ConduitMethodNotFoundException' => 'applications/conduit/protocol/exception/ConduitMethodNotFoundException.php',
'ConduitPHIDListParameterType' => 'applications/conduit/parametertype/ConduitPHIDListParameterType.php',
'ConduitPHIDParameterType' => 'applications/conduit/parametertype/ConduitPHIDParameterType.php',
'ConduitParameterType' => 'applications/conduit/parametertype/ConduitParameterType.php',
'ConduitPingConduitAPIMethod' => 'applications/conduit/method/ConduitPingConduitAPIMethod.php',
'ConduitPointsParameterType' => 'applications/conduit/parametertype/ConduitPointsParameterType.php',
'ConduitProjectListParameterType' => 'applications/conduit/parametertype/ConduitProjectListParameterType.php',
'ConduitQueryConduitAPIMethod' => 'applications/conduit/method/ConduitQueryConduitAPIMethod.php',
'ConduitResultSearchEngineExtension' => 'applications/conduit/query/ConduitResultSearchEngineExtension.php',
'ConduitSSHWorkflow' => 'applications/conduit/ssh/ConduitSSHWorkflow.php',
'ConduitStringListParameterType' => 'applications/conduit/parametertype/ConduitStringListParameterType.php',
'ConduitStringParameterType' => 'applications/conduit/parametertype/ConduitStringParameterType.php',
'ConduitTokenGarbageCollector' => 'applications/conduit/garbagecollector/ConduitTokenGarbageCollector.php',
'ConduitUserListParameterType' => 'applications/conduit/parametertype/ConduitUserListParameterType.php',
'ConduitUserParameterType' => 'applications/conduit/parametertype/ConduitUserParameterType.php',
'ConduitWildParameterType' => 'applications/conduit/parametertype/ConduitWildParameterType.php',
'ConpherenceColumnViewController' => 'applications/conpherence/controller/ConpherenceColumnViewController.php',
'ConpherenceConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceConduitAPIMethod.php',
'ConpherenceConstants' => 'applications/conpherence/constants/ConpherenceConstants.php',
'ConpherenceController' => 'applications/conpherence/controller/ConpherenceController.php',
'ConpherenceCreateThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceCreateThreadConduitAPIMethod.php',
'ConpherenceDAO' => 'applications/conpherence/storage/ConpherenceDAO.php',
'ConpherenceDurableColumnView' => 'applications/conpherence/view/ConpherenceDurableColumnView.php',
'ConpherenceEditConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceEditConduitAPIMethod.php',
'ConpherenceEditEngine' => 'applications/conpherence/editor/ConpherenceEditEngine.php',
'ConpherenceEditor' => 'applications/conpherence/editor/ConpherenceEditor.php',
'ConpherenceFulltextQuery' => 'applications/conpherence/query/ConpherenceFulltextQuery.php',
'ConpherenceIndex' => 'applications/conpherence/storage/ConpherenceIndex.php',
'ConpherenceLayoutView' => 'applications/conpherence/view/ConpherenceLayoutView.php',
'ConpherenceListController' => 'applications/conpherence/controller/ConpherenceListController.php',
'ConpherenceMenuItemView' => 'applications/conpherence/view/ConpherenceMenuItemView.php',
'ConpherenceNotificationPanelController' => 'applications/conpherence/controller/ConpherenceNotificationPanelController.php',
'ConpherenceParticipant' => 'applications/conpherence/storage/ConpherenceParticipant.php',
'ConpherenceParticipantController' => 'applications/conpherence/controller/ConpherenceParticipantController.php',
'ConpherenceParticipantCountQuery' => 'applications/conpherence/query/ConpherenceParticipantCountQuery.php',
'ConpherenceParticipantQuery' => 'applications/conpherence/query/ConpherenceParticipantQuery.php',
'ConpherenceParticipantView' => 'applications/conpherence/view/ConpherenceParticipantView.php',
'ConpherenceQueryThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceQueryThreadConduitAPIMethod.php',
'ConpherenceQueryTransactionConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceQueryTransactionConduitAPIMethod.php',
'ConpherenceReplyHandler' => 'applications/conpherence/mail/ConpherenceReplyHandler.php',
'ConpherenceRoomEditController' => 'applications/conpherence/controller/ConpherenceRoomEditController.php',
'ConpherenceRoomListController' => 'applications/conpherence/controller/ConpherenceRoomListController.php',
'ConpherenceRoomPictureController' => 'applications/conpherence/controller/ConpherenceRoomPictureController.php',
'ConpherenceRoomPreferencesController' => 'applications/conpherence/controller/ConpherenceRoomPreferencesController.php',
'ConpherenceRoomSettings' => 'applications/conpherence/constants/ConpherenceRoomSettings.php',
'ConpherenceRoomTestCase' => 'applications/conpherence/__tests__/ConpherenceRoomTestCase.php',
'ConpherenceSchemaSpec' => 'applications/conpherence/storage/ConpherenceSchemaSpec.php',
'ConpherenceTestCase' => 'applications/conpherence/__tests__/ConpherenceTestCase.php',
'ConpherenceThread' => 'applications/conpherence/storage/ConpherenceThread.php',
'ConpherenceThreadDatasource' => 'applications/conpherence/typeahead/ConpherenceThreadDatasource.php',
'ConpherenceThreadDateMarkerTransaction' => 'applications/conpherence/xaction/ConpherenceThreadDateMarkerTransaction.php',
'ConpherenceThreadIndexEngineExtension' => 'applications/conpherence/engineextension/ConpherenceThreadIndexEngineExtension.php',
'ConpherenceThreadListView' => 'applications/conpherence/view/ConpherenceThreadListView.php',
'ConpherenceThreadMailReceiver' => 'applications/conpherence/mail/ConpherenceThreadMailReceiver.php',
'ConpherenceThreadMembersPolicyRule' => 'applications/conpherence/policyrule/ConpherenceThreadMembersPolicyRule.php',
'ConpherenceThreadParticipantsTransaction' => 'applications/conpherence/xaction/ConpherenceThreadParticipantsTransaction.php',
'ConpherenceThreadPictureTransaction' => 'applications/conpherence/xaction/ConpherenceThreadPictureTransaction.php',
'ConpherenceThreadQuery' => 'applications/conpherence/query/ConpherenceThreadQuery.php',
'ConpherenceThreadRemarkupRule' => 'applications/conpherence/remarkup/ConpherenceThreadRemarkupRule.php',
'ConpherenceThreadSearchController' => 'applications/conpherence/controller/ConpherenceThreadSearchController.php',
'ConpherenceThreadSearchEngine' => 'applications/conpherence/query/ConpherenceThreadSearchEngine.php',
'ConpherenceThreadTitleNgrams' => 'applications/conpherence/storage/ConpherenceThreadTitleNgrams.php',
'ConpherenceThreadTitleTransaction' => 'applications/conpherence/xaction/ConpherenceThreadTitleTransaction.php',
'ConpherenceThreadTopicTransaction' => 'applications/conpherence/xaction/ConpherenceThreadTopicTransaction.php',
'ConpherenceThreadTransactionType' => 'applications/conpherence/xaction/ConpherenceThreadTransactionType.php',
'ConpherenceTransaction' => 'applications/conpherence/storage/ConpherenceTransaction.php',
'ConpherenceTransactionComment' => 'applications/conpherence/storage/ConpherenceTransactionComment.php',
'ConpherenceTransactionQuery' => 'applications/conpherence/query/ConpherenceTransactionQuery.php',
'ConpherenceTransactionRenderer' => 'applications/conpherence/ConpherenceTransactionRenderer.php',
'ConpherenceTransactionView' => 'applications/conpherence/view/ConpherenceTransactionView.php',
'ConpherenceUpdateActions' => 'applications/conpherence/constants/ConpherenceUpdateActions.php',
'ConpherenceUpdateController' => 'applications/conpherence/controller/ConpherenceUpdateController.php',
'ConpherenceUpdateThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceUpdateThreadConduitAPIMethod.php',
'ConpherenceViewController' => 'applications/conpherence/controller/ConpherenceViewController.php',
'CountdownEditConduitAPIMethod' => 'applications/countdown/conduit/CountdownEditConduitAPIMethod.php',
'CountdownSearchConduitAPIMethod' => 'applications/countdown/conduit/CountdownSearchConduitAPIMethod.php',
'DarkConsoleController' => 'applications/console/controller/DarkConsoleController.php',
'DarkConsoleCore' => 'applications/console/core/DarkConsoleCore.php',
'DarkConsoleDataController' => 'applications/console/controller/DarkConsoleDataController.php',
'DarkConsoleErrorLogPlugin' => 'applications/console/plugin/DarkConsoleErrorLogPlugin.php',
'DarkConsoleErrorLogPluginAPI' => 'applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php',
'DarkConsoleEventPlugin' => 'applications/console/plugin/DarkConsoleEventPlugin.php',
'DarkConsoleEventPluginAPI' => 'applications/console/plugin/event/DarkConsoleEventPluginAPI.php',
'DarkConsolePlugin' => 'applications/console/plugin/DarkConsolePlugin.php',
'DarkConsoleRealtimePlugin' => 'applications/console/plugin/DarkConsoleRealtimePlugin.php',
'DarkConsoleRequestPlugin' => 'applications/console/plugin/DarkConsoleRequestPlugin.php',
'DarkConsoleServicesPlugin' => 'applications/console/plugin/DarkConsoleServicesPlugin.php',
'DarkConsoleStartupPlugin' => 'applications/console/plugin/DarkConsoleStartupPlugin.php',
'DarkConsoleXHProfPlugin' => 'applications/console/plugin/DarkConsoleXHProfPlugin.php',
'DarkConsoleXHProfPluginAPI' => 'applications/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php',
'DifferentialAction' => 'applications/differential/constants/DifferentialAction.php',
'DifferentialActionEmailCommand' => 'applications/differential/command/DifferentialActionEmailCommand.php',
'DifferentialAdjustmentMapTestCase' => 'applications/differential/storage/__tests__/DifferentialAdjustmentMapTestCase.php',
'DifferentialAffectedPath' => 'applications/differential/storage/DifferentialAffectedPath.php',
'DifferentialAsanaRepresentationField' => 'applications/differential/customfield/DifferentialAsanaRepresentationField.php',
'DifferentialAuditorsCommitMessageField' => 'applications/differential/field/DifferentialAuditorsCommitMessageField.php',
'DifferentialAuditorsField' => 'applications/differential/customfield/DifferentialAuditorsField.php',
'DifferentialBlameRevisionCommitMessageField' => 'applications/differential/field/DifferentialBlameRevisionCommitMessageField.php',
'DifferentialBlameRevisionField' => 'applications/differential/customfield/DifferentialBlameRevisionField.php',
'DifferentialBlockHeraldAction' => 'applications/differential/herald/DifferentialBlockHeraldAction.php',
'DifferentialBlockingReviewerDatasource' => 'applications/differential/typeahead/DifferentialBlockingReviewerDatasource.php',
'DifferentialBranchField' => 'applications/differential/customfield/DifferentialBranchField.php',
'DifferentialBuildableEngine' => 'applications/differential/harbormaster/DifferentialBuildableEngine.php',
'DifferentialChangeDetailMailView' => 'applications/differential/mail/DifferentialChangeDetailMailView.php',
'DifferentialChangeHeraldFieldGroup' => 'applications/differential/herald/DifferentialChangeHeraldFieldGroup.php',
'DifferentialChangeType' => 'applications/differential/constants/DifferentialChangeType.php',
'DifferentialChangesSinceLastUpdateField' => 'applications/differential/customfield/DifferentialChangesSinceLastUpdateField.php',
'DifferentialChangeset' => 'applications/differential/storage/DifferentialChangeset.php',
'DifferentialChangesetDetailView' => 'applications/differential/view/DifferentialChangesetDetailView.php',
'DifferentialChangesetEngine' => 'applications/differential/engine/DifferentialChangesetEngine.php',
'DifferentialChangesetFileTreeSideNavBuilder' => 'applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php',
'DifferentialChangesetHTMLRenderer' => 'applications/differential/render/DifferentialChangesetHTMLRenderer.php',
'DifferentialChangesetListController' => 'applications/differential/controller/DifferentialChangesetListController.php',
'DifferentialChangesetListView' => 'applications/differential/view/DifferentialChangesetListView.php',
'DifferentialChangesetOneUpMailRenderer' => 'applications/differential/render/DifferentialChangesetOneUpMailRenderer.php',
'DifferentialChangesetOneUpRenderer' => 'applications/differential/render/DifferentialChangesetOneUpRenderer.php',
'DifferentialChangesetOneUpTestRenderer' => 'applications/differential/render/DifferentialChangesetOneUpTestRenderer.php',
'DifferentialChangesetParser' => 'applications/differential/parser/DifferentialChangesetParser.php',
'DifferentialChangesetParserTestCase' => 'applications/differential/parser/__tests__/DifferentialChangesetParserTestCase.php',
'DifferentialChangesetQuery' => 'applications/differential/query/DifferentialChangesetQuery.php',
'DifferentialChangesetRenderer' => 'applications/differential/render/DifferentialChangesetRenderer.php',
'DifferentialChangesetSearchEngine' => 'applications/differential/query/DifferentialChangesetSearchEngine.php',
'DifferentialChangesetTestRenderer' => 'applications/differential/render/DifferentialChangesetTestRenderer.php',
'DifferentialChangesetTwoUpRenderer' => 'applications/differential/render/DifferentialChangesetTwoUpRenderer.php',
'DifferentialChangesetTwoUpTestRenderer' => 'applications/differential/render/DifferentialChangesetTwoUpTestRenderer.php',
'DifferentialChangesetViewController' => 'applications/differential/controller/DifferentialChangesetViewController.php',
'DifferentialCloseConduitAPIMethod' => 'applications/differential/conduit/DifferentialCloseConduitAPIMethod.php',
'DifferentialCommitMessageCustomField' => 'applications/differential/field/DifferentialCommitMessageCustomField.php',
'DifferentialCommitMessageField' => 'applications/differential/field/DifferentialCommitMessageField.php',
'DifferentialCommitMessageFieldTestCase' => 'applications/differential/field/__tests__/DifferentialCommitMessageFieldTestCase.php',
'DifferentialCommitMessageParser' => 'applications/differential/parser/DifferentialCommitMessageParser.php',
'DifferentialCommitMessageParserTestCase' => 'applications/differential/parser/__tests__/DifferentialCommitMessageParserTestCase.php',
'DifferentialCommitsField' => 'applications/differential/customfield/DifferentialCommitsField.php',
'DifferentialCommitsSearchEngineAttachment' => 'applications/differential/engineextension/DifferentialCommitsSearchEngineAttachment.php',
'DifferentialConduitAPIMethod' => 'applications/differential/conduit/DifferentialConduitAPIMethod.php',
'DifferentialConflictsCommitMessageField' => 'applications/differential/field/DifferentialConflictsCommitMessageField.php',
'DifferentialController' => 'applications/differential/controller/DifferentialController.php',
'DifferentialCoreCustomField' => 'applications/differential/customfield/DifferentialCoreCustomField.php',
'DifferentialCreateCommentConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php',
'DifferentialCreateDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php',
'DifferentialCreateInlineConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateInlineConduitAPIMethod.php',
'DifferentialCreateMailReceiver' => 'applications/differential/mail/DifferentialCreateMailReceiver.php',
'DifferentialCreateRawDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php',
'DifferentialCreateRevisionConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateRevisionConduitAPIMethod.php',
'DifferentialCustomField' => 'applications/differential/customfield/DifferentialCustomField.php',
'DifferentialCustomFieldDependsOnParser' => 'applications/differential/parser/DifferentialCustomFieldDependsOnParser.php',
'DifferentialCustomFieldDependsOnParserTestCase' => 'applications/differential/parser/__tests__/DifferentialCustomFieldDependsOnParserTestCase.php',
'DifferentialCustomFieldNumericIndex' => 'applications/differential/storage/DifferentialCustomFieldNumericIndex.php',
'DifferentialCustomFieldRevertsParser' => 'applications/differential/parser/DifferentialCustomFieldRevertsParser.php',
'DifferentialCustomFieldRevertsParserTestCase' => 'applications/differential/parser/__tests__/DifferentialCustomFieldRevertsParserTestCase.php',
'DifferentialCustomFieldStorage' => 'applications/differential/storage/DifferentialCustomFieldStorage.php',
'DifferentialCustomFieldStringIndex' => 'applications/differential/storage/DifferentialCustomFieldStringIndex.php',
'DifferentialDAO' => 'applications/differential/storage/DifferentialDAO.php',
'DifferentialDefaultViewCapability' => 'applications/differential/capability/DifferentialDefaultViewCapability.php',
'DifferentialDiff' => 'applications/differential/storage/DifferentialDiff.php',
'DifferentialDiffAffectedFilesHeraldField' => 'applications/differential/herald/DifferentialDiffAffectedFilesHeraldField.php',
'DifferentialDiffAuthorHeraldField' => 'applications/differential/herald/DifferentialDiffAuthorHeraldField.php',
'DifferentialDiffAuthorProjectsHeraldField' => 'applications/differential/herald/DifferentialDiffAuthorProjectsHeraldField.php',
'DifferentialDiffContentAddedHeraldField' => 'applications/differential/herald/DifferentialDiffContentAddedHeraldField.php',
'DifferentialDiffContentHeraldField' => 'applications/differential/herald/DifferentialDiffContentHeraldField.php',
'DifferentialDiffContentRemovedHeraldField' => 'applications/differential/herald/DifferentialDiffContentRemovedHeraldField.php',
'DifferentialDiffCreateController' => 'applications/differential/controller/DifferentialDiffCreateController.php',
'DifferentialDiffEditor' => 'applications/differential/editor/DifferentialDiffEditor.php',
'DifferentialDiffExtractionEngine' => 'applications/differential/engine/DifferentialDiffExtractionEngine.php',
'DifferentialDiffHeraldField' => 'applications/differential/herald/DifferentialDiffHeraldField.php',
'DifferentialDiffHeraldFieldGroup' => 'applications/differential/herald/DifferentialDiffHeraldFieldGroup.php',
'DifferentialDiffInlineCommentQuery' => 'applications/differential/query/DifferentialDiffInlineCommentQuery.php',
'DifferentialDiffPHIDType' => 'applications/differential/phid/DifferentialDiffPHIDType.php',
'DifferentialDiffProperty' => 'applications/differential/storage/DifferentialDiffProperty.php',
'DifferentialDiffQuery' => 'applications/differential/query/DifferentialDiffQuery.php',
'DifferentialDiffRepositoryHeraldField' => 'applications/differential/herald/DifferentialDiffRepositoryHeraldField.php',
'DifferentialDiffRepositoryProjectsHeraldField' => 'applications/differential/herald/DifferentialDiffRepositoryProjectsHeraldField.php',
'DifferentialDiffSearchConduitAPIMethod' => 'applications/differential/conduit/DifferentialDiffSearchConduitAPIMethod.php',
'DifferentialDiffSearchEngine' => 'applications/differential/query/DifferentialDiffSearchEngine.php',
'DifferentialDiffTestCase' => 'applications/differential/storage/__tests__/DifferentialDiffTestCase.php',
'DifferentialDiffTransaction' => 'applications/differential/storage/DifferentialDiffTransaction.php',
'DifferentialDiffTransactionQuery' => 'applications/differential/query/DifferentialDiffTransactionQuery.php',
'DifferentialDiffViewController' => 'applications/differential/controller/DifferentialDiffViewController.php',
'DifferentialDoorkeeperRevisionFeedStoryPublisher' => 'applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php',
'DifferentialDraftField' => 'applications/differential/customfield/DifferentialDraftField.php',
'DifferentialExactUserFunctionDatasource' => 'applications/differential/typeahead/DifferentialExactUserFunctionDatasource.php',
'DifferentialFieldParseException' => 'applications/differential/exception/DifferentialFieldParseException.php',
'DifferentialFieldValidationException' => 'applications/differential/exception/DifferentialFieldValidationException.php',
'DifferentialGetAllDiffsConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetAllDiffsConduitAPIMethod.php',
'DifferentialGetCommitMessageConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetCommitMessageConduitAPIMethod.php',
'DifferentialGetCommitPathsConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetCommitPathsConduitAPIMethod.php',
'DifferentialGetDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetDiffConduitAPIMethod.php',
'DifferentialGetRawDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetRawDiffConduitAPIMethod.php',
'DifferentialGetRevisionCommentsConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetRevisionCommentsConduitAPIMethod.php',
'DifferentialGetRevisionConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php',
'DifferentialGetWorkingCopy' => 'applications/differential/DifferentialGetWorkingCopy.php',
'DifferentialGitSVNIDCommitMessageField' => 'applications/differential/field/DifferentialGitSVNIDCommitMessageField.php',
'DifferentialHarbormasterField' => 'applications/differential/customfield/DifferentialHarbormasterField.php',
'DifferentialHeraldStateReasons' => 'applications/differential/herald/DifferentialHeraldStateReasons.php',
'DifferentialHiddenComment' => 'applications/differential/storage/DifferentialHiddenComment.php',
'DifferentialHostField' => 'applications/differential/customfield/DifferentialHostField.php',
'DifferentialHovercardEngineExtension' => 'applications/differential/engineextension/DifferentialHovercardEngineExtension.php',
'DifferentialHunk' => 'applications/differential/storage/DifferentialHunk.php',
'DifferentialHunkParser' => 'applications/differential/parser/DifferentialHunkParser.php',
'DifferentialHunkParserTestCase' => 'applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php',
'DifferentialHunkQuery' => 'applications/differential/query/DifferentialHunkQuery.php',
'DifferentialHunkTestCase' => 'applications/differential/storage/__tests__/DifferentialHunkTestCase.php',
'DifferentialInlineComment' => 'applications/differential/storage/DifferentialInlineComment.php',
'DifferentialInlineCommentEditController' => 'applications/differential/controller/DifferentialInlineCommentEditController.php',
'DifferentialInlineCommentMailView' => 'applications/differential/mail/DifferentialInlineCommentMailView.php',
'DifferentialInlineCommentQuery' => 'applications/differential/query/DifferentialInlineCommentQuery.php',
'DifferentialJIRAIssuesCommitMessageField' => 'applications/differential/field/DifferentialJIRAIssuesCommitMessageField.php',
'DifferentialJIRAIssuesField' => 'applications/differential/customfield/DifferentialJIRAIssuesField.php',
'DifferentialLegacyQuery' => 'applications/differential/constants/DifferentialLegacyQuery.php',
'DifferentialLineAdjustmentMap' => 'applications/differential/parser/DifferentialLineAdjustmentMap.php',
'DifferentialLintField' => 'applications/differential/customfield/DifferentialLintField.php',
'DifferentialLintStatus' => 'applications/differential/constants/DifferentialLintStatus.php',
'DifferentialLocalCommitsView' => 'applications/differential/view/DifferentialLocalCommitsView.php',
'DifferentialMailEngineExtension' => 'applications/differential/engineextension/DifferentialMailEngineExtension.php',
'DifferentialMailView' => 'applications/differential/mail/DifferentialMailView.php',
'DifferentialManiphestTasksField' => 'applications/differential/customfield/DifferentialManiphestTasksField.php',
'DifferentialParseCacheGarbageCollector' => 'applications/differential/garbagecollector/DifferentialParseCacheGarbageCollector.php',
'DifferentialParseCommitMessageConduitAPIMethod' => 'applications/differential/conduit/DifferentialParseCommitMessageConduitAPIMethod.php',
'DifferentialParseRenderTestCase' => 'applications/differential/__tests__/DifferentialParseRenderTestCase.php',
'DifferentialPathField' => 'applications/differential/customfield/DifferentialPathField.php',
'DifferentialProjectReviewersField' => 'applications/differential/customfield/DifferentialProjectReviewersField.php',
'DifferentialQueryConduitAPIMethod' => 'applications/differential/conduit/DifferentialQueryConduitAPIMethod.php',
'DifferentialQueryDiffsConduitAPIMethod' => 'applications/differential/conduit/DifferentialQueryDiffsConduitAPIMethod.php',
'DifferentialRawDiffRenderer' => 'applications/differential/render/DifferentialRawDiffRenderer.php',
'DifferentialReleephRequestFieldSpecification' => 'applications/releeph/differential/DifferentialReleephRequestFieldSpecification.php',
'DifferentialRemarkupRule' => 'applications/differential/remarkup/DifferentialRemarkupRule.php',
'DifferentialReplyHandler' => 'applications/differential/mail/DifferentialReplyHandler.php',
'DifferentialRepositoryField' => 'applications/differential/customfield/DifferentialRepositoryField.php',
'DifferentialRepositoryLookup' => 'applications/differential/query/DifferentialRepositoryLookup.php',
'DifferentialRequiredSignaturesField' => 'applications/differential/customfield/DifferentialRequiredSignaturesField.php',
'DifferentialResponsibleDatasource' => 'applications/differential/typeahead/DifferentialResponsibleDatasource.php',
'DifferentialResponsibleUserDatasource' => 'applications/differential/typeahead/DifferentialResponsibleUserDatasource.php',
'DifferentialResponsibleViewerFunctionDatasource' => 'applications/differential/typeahead/DifferentialResponsibleViewerFunctionDatasource.php',
'DifferentialRevertPlanCommitMessageField' => 'applications/differential/field/DifferentialRevertPlanCommitMessageField.php',
'DifferentialRevertPlanField' => 'applications/differential/customfield/DifferentialRevertPlanField.php',
'DifferentialReviewedByCommitMessageField' => 'applications/differential/field/DifferentialReviewedByCommitMessageField.php',
'DifferentialReviewer' => 'applications/differential/storage/DifferentialReviewer.php',
'DifferentialReviewerDatasource' => 'applications/differential/typeahead/DifferentialReviewerDatasource.php',
'DifferentialReviewerForRevisionEdgeType' => 'applications/differential/edge/DifferentialReviewerForRevisionEdgeType.php',
'DifferentialReviewerStatus' => 'applications/differential/constants/DifferentialReviewerStatus.php',
'DifferentialReviewersAddBlockingReviewersHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddBlockingReviewersHeraldAction.php',
'DifferentialReviewersAddBlockingSelfHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddBlockingSelfHeraldAction.php',
'DifferentialReviewersAddReviewersHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddReviewersHeraldAction.php',
'DifferentialReviewersAddSelfHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddSelfHeraldAction.php',
'DifferentialReviewersCommitMessageField' => 'applications/differential/field/DifferentialReviewersCommitMessageField.php',
'DifferentialReviewersField' => 'applications/differential/customfield/DifferentialReviewersField.php',
'DifferentialReviewersHeraldAction' => 'applications/differential/herald/DifferentialReviewersHeraldAction.php',
'DifferentialReviewersSearchEngineAttachment' => 'applications/differential/engineextension/DifferentialReviewersSearchEngineAttachment.php',
'DifferentialReviewersView' => 'applications/differential/view/DifferentialReviewersView.php',
'DifferentialRevision' => 'applications/differential/storage/DifferentialRevision.php',
'DifferentialRevisionAbandonTransaction' => 'applications/differential/xaction/DifferentialRevisionAbandonTransaction.php',
'DifferentialRevisionAcceptTransaction' => 'applications/differential/xaction/DifferentialRevisionAcceptTransaction.php',
'DifferentialRevisionActionTransaction' => 'applications/differential/xaction/DifferentialRevisionActionTransaction.php',
'DifferentialRevisionAffectedFilesHeraldField' => 'applications/differential/herald/DifferentialRevisionAffectedFilesHeraldField.php',
'DifferentialRevisionAuthorHeraldField' => 'applications/differential/herald/DifferentialRevisionAuthorHeraldField.php',
'DifferentialRevisionAuthorProjectsHeraldField' => 'applications/differential/herald/DifferentialRevisionAuthorProjectsHeraldField.php',
'DifferentialRevisionBuildableTransaction' => 'applications/differential/xaction/DifferentialRevisionBuildableTransaction.php',
'DifferentialRevisionCloseDetailsController' => 'applications/differential/controller/DifferentialRevisionCloseDetailsController.php',
'DifferentialRevisionCloseTransaction' => 'applications/differential/xaction/DifferentialRevisionCloseTransaction.php',
'DifferentialRevisionClosedStatusDatasource' => 'applications/differential/typeahead/DifferentialRevisionClosedStatusDatasource.php',
'DifferentialRevisionCommandeerTransaction' => 'applications/differential/xaction/DifferentialRevisionCommandeerTransaction.php',
'DifferentialRevisionContentAddedHeraldField' => 'applications/differential/herald/DifferentialRevisionContentAddedHeraldField.php',
'DifferentialRevisionContentHeraldField' => 'applications/differential/herald/DifferentialRevisionContentHeraldField.php',
'DifferentialRevisionContentRemovedHeraldField' => 'applications/differential/herald/DifferentialRevisionContentRemovedHeraldField.php',
'DifferentialRevisionControlSystem' => 'applications/differential/constants/DifferentialRevisionControlSystem.php',
'DifferentialRevisionDependedOnByRevisionEdgeType' => 'applications/differential/edge/DifferentialRevisionDependedOnByRevisionEdgeType.php',
'DifferentialRevisionDependsOnRevisionEdgeType' => 'applications/differential/edge/DifferentialRevisionDependsOnRevisionEdgeType.php',
'DifferentialRevisionDraftEngine' => 'applications/differential/engine/DifferentialRevisionDraftEngine.php',
'DifferentialRevisionEditConduitAPIMethod' => 'applications/differential/conduit/DifferentialRevisionEditConduitAPIMethod.php',
'DifferentialRevisionEditController' => 'applications/differential/controller/DifferentialRevisionEditController.php',
'DifferentialRevisionEditEngine' => 'applications/differential/editor/DifferentialRevisionEditEngine.php',
'DifferentialRevisionFerretEngine' => 'applications/differential/search/DifferentialRevisionFerretEngine.php',
'DifferentialRevisionFulltextEngine' => 'applications/differential/search/DifferentialRevisionFulltextEngine.php',
'DifferentialRevisionGraph' => 'infrastructure/graph/DifferentialRevisionGraph.php',
'DifferentialRevisionHasChildRelationship' => 'applications/differential/relationships/DifferentialRevisionHasChildRelationship.php',
'DifferentialRevisionHasCommitEdgeType' => 'applications/differential/edge/DifferentialRevisionHasCommitEdgeType.php',
'DifferentialRevisionHasCommitRelationship' => 'applications/differential/relationships/DifferentialRevisionHasCommitRelationship.php',
'DifferentialRevisionHasParentRelationship' => 'applications/differential/relationships/DifferentialRevisionHasParentRelationship.php',
'DifferentialRevisionHasReviewerEdgeType' => 'applications/differential/edge/DifferentialRevisionHasReviewerEdgeType.php',
'DifferentialRevisionHasTaskEdgeType' => 'applications/differential/edge/DifferentialRevisionHasTaskEdgeType.php',
'DifferentialRevisionHasTaskRelationship' => 'applications/differential/relationships/DifferentialRevisionHasTaskRelationship.php',
'DifferentialRevisionHeraldField' => 'applications/differential/herald/DifferentialRevisionHeraldField.php',
'DifferentialRevisionHeraldFieldGroup' => 'applications/differential/herald/DifferentialRevisionHeraldFieldGroup.php',
'DifferentialRevisionHoldDraftTransaction' => 'applications/differential/xaction/DifferentialRevisionHoldDraftTransaction.php',
'DifferentialRevisionIDCommitMessageField' => 'applications/differential/field/DifferentialRevisionIDCommitMessageField.php',
'DifferentialRevisionInlineTransaction' => 'applications/differential/xaction/DifferentialRevisionInlineTransaction.php',
'DifferentialRevisionInlinesController' => 'applications/differential/controller/DifferentialRevisionInlinesController.php',
'DifferentialRevisionListController' => 'applications/differential/controller/DifferentialRevisionListController.php',
'DifferentialRevisionListView' => 'applications/differential/view/DifferentialRevisionListView.php',
'DifferentialRevisionMailReceiver' => 'applications/differential/mail/DifferentialRevisionMailReceiver.php',
'DifferentialRevisionOpenStatusDatasource' => 'applications/differential/typeahead/DifferentialRevisionOpenStatusDatasource.php',
'DifferentialRevisionOperationController' => 'applications/differential/controller/DifferentialRevisionOperationController.php',
'DifferentialRevisionPHIDType' => 'applications/differential/phid/DifferentialRevisionPHIDType.php',
'DifferentialRevisionPackageHeraldField' => 'applications/differential/herald/DifferentialRevisionPackageHeraldField.php',
'DifferentialRevisionPackageOwnerHeraldField' => 'applications/differential/herald/DifferentialRevisionPackageOwnerHeraldField.php',
'DifferentialRevisionPlanChangesTransaction' => 'applications/differential/xaction/DifferentialRevisionPlanChangesTransaction.php',
'DifferentialRevisionQuery' => 'applications/differential/query/DifferentialRevisionQuery.php',
'DifferentialRevisionReclaimTransaction' => 'applications/differential/xaction/DifferentialRevisionReclaimTransaction.php',
'DifferentialRevisionRejectTransaction' => 'applications/differential/xaction/DifferentialRevisionRejectTransaction.php',
'DifferentialRevisionRelationship' => 'applications/differential/relationships/DifferentialRevisionRelationship.php',
'DifferentialRevisionRelationshipSource' => 'applications/search/relationship/DifferentialRevisionRelationshipSource.php',
'DifferentialRevisionReopenTransaction' => 'applications/differential/xaction/DifferentialRevisionReopenTransaction.php',
'DifferentialRevisionRepositoryHeraldField' => 'applications/differential/herald/DifferentialRevisionRepositoryHeraldField.php',
'DifferentialRevisionRepositoryProjectsHeraldField' => 'applications/differential/herald/DifferentialRevisionRepositoryProjectsHeraldField.php',
'DifferentialRevisionRepositoryTransaction' => 'applications/differential/xaction/DifferentialRevisionRepositoryTransaction.php',
'DifferentialRevisionRequestReviewTransaction' => 'applications/differential/xaction/DifferentialRevisionRequestReviewTransaction.php',
'DifferentialRevisionRequiredActionResultBucket' => 'applications/differential/query/DifferentialRevisionRequiredActionResultBucket.php',
'DifferentialRevisionResignTransaction' => 'applications/differential/xaction/DifferentialRevisionResignTransaction.php',
'DifferentialRevisionResultBucket' => 'applications/differential/query/DifferentialRevisionResultBucket.php',
'DifferentialRevisionReviewTransaction' => 'applications/differential/xaction/DifferentialRevisionReviewTransaction.php',
'DifferentialRevisionReviewersHeraldField' => 'applications/differential/herald/DifferentialRevisionReviewersHeraldField.php',
'DifferentialRevisionReviewersTransaction' => 'applications/differential/xaction/DifferentialRevisionReviewersTransaction.php',
'DifferentialRevisionSearchConduitAPIMethod' => 'applications/differential/conduit/DifferentialRevisionSearchConduitAPIMethod.php',
'DifferentialRevisionSearchEngine' => 'applications/differential/query/DifferentialRevisionSearchEngine.php',
'DifferentialRevisionStatus' => 'applications/differential/constants/DifferentialRevisionStatus.php',
'DifferentialRevisionStatusDatasource' => 'applications/differential/typeahead/DifferentialRevisionStatusDatasource.php',
'DifferentialRevisionStatusFunctionDatasource' => 'applications/differential/typeahead/DifferentialRevisionStatusFunctionDatasource.php',
'DifferentialRevisionStatusHeraldField' => 'applications/differential/herald/DifferentialRevisionStatusHeraldField.php',
'DifferentialRevisionStatusTransaction' => 'applications/differential/xaction/DifferentialRevisionStatusTransaction.php',
'DifferentialRevisionSummaryHeraldField' => 'applications/differential/herald/DifferentialRevisionSummaryHeraldField.php',
'DifferentialRevisionSummaryTransaction' => 'applications/differential/xaction/DifferentialRevisionSummaryTransaction.php',
'DifferentialRevisionTestPlanHeraldField' => 'applications/differential/herald/DifferentialRevisionTestPlanHeraldField.php',
'DifferentialRevisionTestPlanTransaction' => 'applications/differential/xaction/DifferentialRevisionTestPlanTransaction.php',
'DifferentialRevisionTimelineEngine' => 'applications/differential/engine/DifferentialRevisionTimelineEngine.php',
'DifferentialRevisionTitleHeraldField' => 'applications/differential/herald/DifferentialRevisionTitleHeraldField.php',
'DifferentialRevisionTitleTransaction' => 'applications/differential/xaction/DifferentialRevisionTitleTransaction.php',
'DifferentialRevisionTransactionType' => 'applications/differential/xaction/DifferentialRevisionTransactionType.php',
'DifferentialRevisionUpdateHistoryView' => 'applications/differential/view/DifferentialRevisionUpdateHistoryView.php',
'DifferentialRevisionUpdateTransaction' => 'applications/differential/xaction/DifferentialRevisionUpdateTransaction.php',
'DifferentialRevisionViewController' => 'applications/differential/controller/DifferentialRevisionViewController.php',
'DifferentialRevisionVoidTransaction' => 'applications/differential/xaction/DifferentialRevisionVoidTransaction.php',
+ 'DifferentialRevisionWrongBuildsTransaction' => 'applications/differential/xaction/DifferentialRevisionWrongBuildsTransaction.php',
'DifferentialRevisionWrongStateTransaction' => 'applications/differential/xaction/DifferentialRevisionWrongStateTransaction.php',
'DifferentialSchemaSpec' => 'applications/differential/storage/DifferentialSchemaSpec.php',
'DifferentialSetDiffPropertyConduitAPIMethod' => 'applications/differential/conduit/DifferentialSetDiffPropertyConduitAPIMethod.php',
'DifferentialStoredCustomField' => 'applications/differential/customfield/DifferentialStoredCustomField.php',
'DifferentialSubscribersCommitMessageField' => 'applications/differential/field/DifferentialSubscribersCommitMessageField.php',
'DifferentialSummaryCommitMessageField' => 'applications/differential/field/DifferentialSummaryCommitMessageField.php',
'DifferentialSummaryField' => 'applications/differential/customfield/DifferentialSummaryField.php',
'DifferentialTagsCommitMessageField' => 'applications/differential/field/DifferentialTagsCommitMessageField.php',
'DifferentialTasksCommitMessageField' => 'applications/differential/field/DifferentialTasksCommitMessageField.php',
'DifferentialTestPlanCommitMessageField' => 'applications/differential/field/DifferentialTestPlanCommitMessageField.php',
'DifferentialTestPlanField' => 'applications/differential/customfield/DifferentialTestPlanField.php',
'DifferentialTitleCommitMessageField' => 'applications/differential/field/DifferentialTitleCommitMessageField.php',
'DifferentialTransaction' => 'applications/differential/storage/DifferentialTransaction.php',
'DifferentialTransactionComment' => 'applications/differential/storage/DifferentialTransactionComment.php',
'DifferentialTransactionEditor' => 'applications/differential/editor/DifferentialTransactionEditor.php',
'DifferentialTransactionQuery' => 'applications/differential/query/DifferentialTransactionQuery.php',
'DifferentialTransactionView' => 'applications/differential/view/DifferentialTransactionView.php',
'DifferentialUnitField' => 'applications/differential/customfield/DifferentialUnitField.php',
'DifferentialUnitStatus' => 'applications/differential/constants/DifferentialUnitStatus.php',
'DifferentialUnitTestResult' => 'applications/differential/constants/DifferentialUnitTestResult.php',
'DifferentialUpdateRevisionConduitAPIMethod' => 'applications/differential/conduit/DifferentialUpdateRevisionConduitAPIMethod.php',
'DiffusionAuditorDatasource' => 'applications/diffusion/typeahead/DiffusionAuditorDatasource.php',
'DiffusionAuditorFunctionDatasource' => 'applications/diffusion/typeahead/DiffusionAuditorFunctionDatasource.php',
'DiffusionAuditorsAddAuditorsHeraldAction' => 'applications/diffusion/herald/DiffusionAuditorsAddAuditorsHeraldAction.php',
'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',
'DiffusionBranchQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionBranchQueryConduitAPIMethod.php',
'DiffusionBranchTableController' => 'applications/diffusion/controller/DiffusionBranchTableController.php',
'DiffusionBranchTableView' => 'applications/diffusion/view/DiffusionBranchTableView.php',
'DiffusionBrowseController' => 'applications/diffusion/controller/DiffusionBrowseController.php',
'DiffusionBrowseQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php',
'DiffusionBrowseResultSet' => 'applications/diffusion/data/DiffusionBrowseResultSet.php',
'DiffusionBrowseTableView' => 'applications/diffusion/view/DiffusionBrowseTableView.php',
'DiffusionBuildableEngine' => 'applications/diffusion/harbormaster/DiffusionBuildableEngine.php',
'DiffusionCacheEngineExtension' => 'applications/diffusion/engineextension/DiffusionCacheEngineExtension.php',
'DiffusionCachedResolveRefsQuery' => 'applications/diffusion/query/DiffusionCachedResolveRefsQuery.php',
'DiffusionChangeController' => 'applications/diffusion/controller/DiffusionChangeController.php',
'DiffusionChangeHeraldFieldGroup' => 'applications/diffusion/herald/DiffusionChangeHeraldFieldGroup.php',
'DiffusionCloneController' => 'applications/diffusion/controller/DiffusionCloneController.php',
'DiffusionCloneURIView' => 'applications/diffusion/view/DiffusionCloneURIView.php',
'DiffusionCommandEngine' => 'applications/diffusion/protocol/DiffusionCommandEngine.php',
'DiffusionCommandEngineTestCase' => 'applications/diffusion/protocol/__tests__/DiffusionCommandEngineTestCase.php',
'DiffusionCommitAcceptTransaction' => 'applications/diffusion/xaction/DiffusionCommitAcceptTransaction.php',
'DiffusionCommitActionTransaction' => 'applications/diffusion/xaction/DiffusionCommitActionTransaction.php',
'DiffusionCommitAffectedFilesHeraldField' => 'applications/diffusion/herald/DiffusionCommitAffectedFilesHeraldField.php',
'DiffusionCommitAuditStatus' => 'applications/diffusion/DiffusionCommitAuditStatus.php',
'DiffusionCommitAuditTransaction' => 'applications/diffusion/xaction/DiffusionCommitAuditTransaction.php',
'DiffusionCommitAuditorsHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuditorsHeraldField.php',
'DiffusionCommitAuditorsTransaction' => 'applications/diffusion/xaction/DiffusionCommitAuditorsTransaction.php',
'DiffusionCommitAuthorHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuthorHeraldField.php',
'DiffusionCommitAuthorProjectsHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuthorProjectsHeraldField.php',
'DiffusionCommitAutocloseHeraldField' => 'applications/diffusion/herald/DiffusionCommitAutocloseHeraldField.php',
'DiffusionCommitBranchesController' => 'applications/diffusion/controller/DiffusionCommitBranchesController.php',
'DiffusionCommitBranchesHeraldField' => 'applications/diffusion/herald/DiffusionCommitBranchesHeraldField.php',
'DiffusionCommitBuildableTransaction' => 'applications/diffusion/xaction/DiffusionCommitBuildableTransaction.php',
'DiffusionCommitCommitterHeraldField' => 'applications/diffusion/herald/DiffusionCommitCommitterHeraldField.php',
'DiffusionCommitCommitterProjectsHeraldField' => 'applications/diffusion/herald/DiffusionCommitCommitterProjectsHeraldField.php',
'DiffusionCommitConcernTransaction' => 'applications/diffusion/xaction/DiffusionCommitConcernTransaction.php',
'DiffusionCommitController' => 'applications/diffusion/controller/DiffusionCommitController.php',
'DiffusionCommitDiffContentAddedHeraldField' => 'applications/diffusion/herald/DiffusionCommitDiffContentAddedHeraldField.php',
'DiffusionCommitDiffContentHeraldField' => 'applications/diffusion/herald/DiffusionCommitDiffContentHeraldField.php',
'DiffusionCommitDiffContentRemovedHeraldField' => 'applications/diffusion/herald/DiffusionCommitDiffContentRemovedHeraldField.php',
'DiffusionCommitDiffEnormousHeraldField' => 'applications/diffusion/herald/DiffusionCommitDiffEnormousHeraldField.php',
'DiffusionCommitDraftEngine' => 'applications/diffusion/engine/DiffusionCommitDraftEngine.php',
'DiffusionCommitEditConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionCommitEditConduitAPIMethod.php',
'DiffusionCommitEditController' => 'applications/diffusion/controller/DiffusionCommitEditController.php',
'DiffusionCommitEditEngine' => 'applications/diffusion/editor/DiffusionCommitEditEngine.php',
'DiffusionCommitFerretEngine' => 'applications/repository/search/DiffusionCommitFerretEngine.php',
'DiffusionCommitFulltextEngine' => 'applications/repository/search/DiffusionCommitFulltextEngine.php',
'DiffusionCommitHasPackageEdgeType' => 'applications/diffusion/edge/DiffusionCommitHasPackageEdgeType.php',
'DiffusionCommitHasRevisionEdgeType' => 'applications/diffusion/edge/DiffusionCommitHasRevisionEdgeType.php',
'DiffusionCommitHasRevisionRelationship' => 'applications/diffusion/relationships/DiffusionCommitHasRevisionRelationship.php',
'DiffusionCommitHasTaskEdgeType' => 'applications/diffusion/edge/DiffusionCommitHasTaskEdgeType.php',
'DiffusionCommitHasTaskRelationship' => 'applications/diffusion/relationships/DiffusionCommitHasTaskRelationship.php',
'DiffusionCommitHash' => 'applications/diffusion/data/DiffusionCommitHash.php',
'DiffusionCommitHeraldField' => 'applications/diffusion/herald/DiffusionCommitHeraldField.php',
'DiffusionCommitHeraldFieldGroup' => 'applications/diffusion/herald/DiffusionCommitHeraldFieldGroup.php',
'DiffusionCommitHintQuery' => 'applications/diffusion/query/DiffusionCommitHintQuery.php',
'DiffusionCommitHookEngine' => 'applications/diffusion/engine/DiffusionCommitHookEngine.php',
'DiffusionCommitHookRejectException' => 'applications/diffusion/exception/DiffusionCommitHookRejectException.php',
'DiffusionCommitListController' => 'applications/diffusion/controller/DiffusionCommitListController.php',
'DiffusionCommitListView' => 'applications/diffusion/view/DiffusionCommitListView.php',
'DiffusionCommitMergeHeraldField' => 'applications/diffusion/herald/DiffusionCommitMergeHeraldField.php',
'DiffusionCommitMessageHeraldField' => 'applications/diffusion/herald/DiffusionCommitMessageHeraldField.php',
'DiffusionCommitPackageAuditHeraldField' => 'applications/diffusion/herald/DiffusionCommitPackageAuditHeraldField.php',
'DiffusionCommitPackageHeraldField' => 'applications/diffusion/herald/DiffusionCommitPackageHeraldField.php',
'DiffusionCommitPackageOwnerHeraldField' => 'applications/diffusion/herald/DiffusionCommitPackageOwnerHeraldField.php',
'DiffusionCommitParentsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionCommitParentsQueryConduitAPIMethod.php',
'DiffusionCommitQuery' => 'applications/diffusion/query/DiffusionCommitQuery.php',
'DiffusionCommitRef' => 'applications/diffusion/data/DiffusionCommitRef.php',
'DiffusionCommitRelationship' => 'applications/diffusion/relationships/DiffusionCommitRelationship.php',
'DiffusionCommitRelationshipSource' => 'applications/search/relationship/DiffusionCommitRelationshipSource.php',
'DiffusionCommitRemarkupRule' => 'applications/diffusion/remarkup/DiffusionCommitRemarkupRule.php',
'DiffusionCommitRemarkupRuleTestCase' => 'applications/diffusion/remarkup/__tests__/DiffusionCommitRemarkupRuleTestCase.php',
'DiffusionCommitRepositoryHeraldField' => 'applications/diffusion/herald/DiffusionCommitRepositoryHeraldField.php',
'DiffusionCommitRepositoryProjectsHeraldField' => 'applications/diffusion/herald/DiffusionCommitRepositoryProjectsHeraldField.php',
'DiffusionCommitRequiredActionResultBucket' => 'applications/diffusion/query/DiffusionCommitRequiredActionResultBucket.php',
'DiffusionCommitResignTransaction' => 'applications/diffusion/xaction/DiffusionCommitResignTransaction.php',
'DiffusionCommitResultBucket' => 'applications/diffusion/query/DiffusionCommitResultBucket.php',
'DiffusionCommitRevertedByCommitEdgeType' => 'applications/diffusion/edge/DiffusionCommitRevertedByCommitEdgeType.php',
'DiffusionCommitRevertsCommitEdgeType' => 'applications/diffusion/edge/DiffusionCommitRevertsCommitEdgeType.php',
'DiffusionCommitReviewerHeraldField' => 'applications/diffusion/herald/DiffusionCommitReviewerHeraldField.php',
'DiffusionCommitRevisionAcceptedHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionAcceptedHeraldField.php',
'DiffusionCommitRevisionAcceptingReviewersHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionAcceptingReviewersHeraldField.php',
'DiffusionCommitRevisionHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionHeraldField.php',
'DiffusionCommitRevisionReviewersHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionReviewersHeraldField.php',
'DiffusionCommitRevisionSubscribersHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionSubscribersHeraldField.php',
'DiffusionCommitSearchConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionCommitSearchConduitAPIMethod.php',
'DiffusionCommitStateTransaction' => 'applications/diffusion/xaction/DiffusionCommitStateTransaction.php',
'DiffusionCommitTagsController' => 'applications/diffusion/controller/DiffusionCommitTagsController.php',
'DiffusionCommitTimelineEngine' => 'applications/diffusion/engine/DiffusionCommitTimelineEngine.php',
'DiffusionCommitTransactionType' => 'applications/diffusion/xaction/DiffusionCommitTransactionType.php',
'DiffusionCommitVerifyTransaction' => 'applications/diffusion/xaction/DiffusionCommitVerifyTransaction.php',
'DiffusionCompareController' => 'applications/diffusion/controller/DiffusionCompareController.php',
'DiffusionConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionConduitAPIMethod.php',
'DiffusionController' => 'applications/diffusion/controller/DiffusionController.php',
'DiffusionCreateRepositoriesCapability' => 'applications/diffusion/capability/DiffusionCreateRepositoriesCapability.php',
'DiffusionDaemonLockException' => 'applications/diffusion/exception/DiffusionDaemonLockException.php',
'DiffusionDatasourceEngineExtension' => 'applications/diffusion/engineextension/DiffusionDatasourceEngineExtension.php',
'DiffusionDefaultEditCapability' => 'applications/diffusion/capability/DiffusionDefaultEditCapability.php',
'DiffusionDefaultPushCapability' => 'applications/diffusion/capability/DiffusionDefaultPushCapability.php',
'DiffusionDefaultViewCapability' => 'applications/diffusion/capability/DiffusionDefaultViewCapability.php',
'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',
'DiffusionExternalController' => 'applications/diffusion/controller/DiffusionExternalController.php',
'DiffusionExternalSymbolQuery' => 'applications/diffusion/symbol/DiffusionExternalSymbolQuery.php',
'DiffusionExternalSymbolsSource' => 'applications/diffusion/symbol/DiffusionExternalSymbolsSource.php',
'DiffusionFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionFileContentQuery.php',
'DiffusionFileContentQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionFileContentQueryConduitAPIMethod.php',
'DiffusionFileFutureQuery' => 'applications/diffusion/query/DiffusionFileFutureQuery.php',
'DiffusionFindSymbolsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionFindSymbolsConduitAPIMethod.php',
'DiffusionGetLintMessagesConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionGetLintMessagesConduitAPIMethod.php',
'DiffusionGetRecentCommitsByPathConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionGetRecentCommitsByPathConduitAPIMethod.php',
'DiffusionGitBlameQuery' => 'applications/diffusion/query/blame/DiffusionGitBlameQuery.php',
'DiffusionGitBranch' => 'applications/diffusion/data/DiffusionGitBranch.php',
'DiffusionGitBranchTestCase' => 'applications/diffusion/data/__tests__/DiffusionGitBranchTestCase.php',
'DiffusionGitCommandEngine' => 'applications/diffusion/protocol/DiffusionGitCommandEngine.php',
'DiffusionGitFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php',
'DiffusionGitLFSAuthenticateWorkflow' => 'applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php',
'DiffusionGitLFSResponse' => 'applications/diffusion/response/DiffusionGitLFSResponse.php',
'DiffusionGitLFSTemporaryTokenType' => 'applications/diffusion/gitlfs/DiffusionGitLFSTemporaryTokenType.php',
'DiffusionGitRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php',
'DiffusionGitReceivePackSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php',
'DiffusionGitRequest' => 'applications/diffusion/request/DiffusionGitRequest.php',
'DiffusionGitResponse' => 'applications/diffusion/response/DiffusionGitResponse.php',
'DiffusionGitSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitSSHWorkflow.php',
'DiffusionGitUploadPackSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php',
'DiffusionGraphController' => 'applications/diffusion/controller/DiffusionGraphController.php',
'DiffusionHistoryController' => 'applications/diffusion/controller/DiffusionHistoryController.php',
'DiffusionHistoryListView' => 'applications/diffusion/view/DiffusionHistoryListView.php',
'DiffusionHistoryQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php',
'DiffusionHistoryTableView' => 'applications/diffusion/view/DiffusionHistoryTableView.php',
'DiffusionHistoryView' => 'applications/diffusion/view/DiffusionHistoryView.php',
'DiffusionHovercardEngineExtension' => 'applications/diffusion/engineextension/DiffusionHovercardEngineExtension.php',
'DiffusionIdentityAssigneeDatasource' => 'applications/diffusion/typeahead/DiffusionIdentityAssigneeDatasource.php',
'DiffusionIdentityAssigneeEditField' => 'applications/diffusion/editfield/DiffusionIdentityAssigneeEditField.php',
'DiffusionIdentityAssigneeSearchField' => 'applications/diffusion/searchfield/DiffusionIdentityAssigneeSearchField.php',
'DiffusionIdentityEditController' => 'applications/diffusion/controller/DiffusionIdentityEditController.php',
'DiffusionIdentityListController' => 'applications/diffusion/controller/DiffusionIdentityListController.php',
'DiffusionIdentityUnassignedDatasource' => 'applications/diffusion/typeahead/DiffusionIdentityUnassignedDatasource.php',
'DiffusionIdentityViewController' => 'applications/diffusion/controller/DiffusionIdentityViewController.php',
'DiffusionInlineCommentController' => 'applications/diffusion/controller/DiffusionInlineCommentController.php',
'DiffusionInlineCommentPreviewController' => 'applications/diffusion/controller/DiffusionInlineCommentPreviewController.php',
'DiffusionInternalAncestorsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionInternalAncestorsConduitAPIMethod.php',
'DiffusionInternalGitRawDiffQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionInternalGitRawDiffQueryConduitAPIMethod.php',
'DiffusionLastModifiedController' => 'applications/diffusion/controller/DiffusionLastModifiedController.php',
'DiffusionLastModifiedQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionLastModifiedQueryConduitAPIMethod.php',
'DiffusionLintController' => 'applications/diffusion/controller/DiffusionLintController.php',
'DiffusionLintCountQuery' => 'applications/diffusion/query/DiffusionLintCountQuery.php',
'DiffusionLintSaveRunner' => 'applications/diffusion/DiffusionLintSaveRunner.php',
'DiffusionLocalRepositoryFilter' => 'applications/diffusion/data/DiffusionLocalRepositoryFilter.php',
'DiffusionLogController' => 'applications/diffusion/controller/DiffusionLogController.php',
'DiffusionLookSoonConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionLookSoonConduitAPIMethod.php',
'DiffusionLowLevelCommitFieldsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelCommitFieldsQuery.php',
'DiffusionLowLevelCommitQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php',
'DiffusionLowLevelFilesizeQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelFilesizeQuery.php',
'DiffusionLowLevelGitRefQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelGitRefQuery.php',
'DiffusionLowLevelMercurialBranchesQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialBranchesQuery.php',
'DiffusionLowLevelMercurialPathsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialPathsQuery.php',
'DiffusionLowLevelParentsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php',
'DiffusionLowLevelQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php',
'DiffusionLowLevelResolveRefsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php',
'DiffusionMercurialBlameQuery' => 'applications/diffusion/query/blame/DiffusionMercurialBlameQuery.php',
'DiffusionMercurialCommandEngine' => 'applications/diffusion/protocol/DiffusionMercurialCommandEngine.php',
'DiffusionMercurialFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionMercurialFileContentQuery.php',
'DiffusionMercurialFlagInjectionException' => 'applications/diffusion/exception/DiffusionMercurialFlagInjectionException.php',
'DiffusionMercurialRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php',
'DiffusionMercurialRequest' => 'applications/diffusion/request/DiffusionMercurialRequest.php',
'DiffusionMercurialResponse' => 'applications/diffusion/response/DiffusionMercurialResponse.php',
'DiffusionMercurialSSHWorkflow' => 'applications/diffusion/ssh/DiffusionMercurialSSHWorkflow.php',
'DiffusionMercurialServeSSHWorkflow' => 'applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php',
'DiffusionMercurialWireClientSSHProtocolChannel' => 'applications/diffusion/ssh/DiffusionMercurialWireClientSSHProtocolChannel.php',
'DiffusionMercurialWireProtocol' => 'applications/diffusion/protocol/DiffusionMercurialWireProtocol.php',
'DiffusionMercurialWireProtocolTests' => 'applications/diffusion/protocol/__tests__/DiffusionMercurialWireProtocolTests.php',
'DiffusionMercurialWireSSHTestCase' => 'applications/diffusion/ssh/__tests__/DiffusionMercurialWireSSHTestCase.php',
'DiffusionMergedCommitsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionMergedCommitsQueryConduitAPIMethod.php',
'DiffusionPathChange' => 'applications/diffusion/data/DiffusionPathChange.php',
'DiffusionPathChangeQuery' => 'applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php',
'DiffusionPathCompleteController' => 'applications/diffusion/controller/DiffusionPathCompleteController.php',
'DiffusionPathIDQuery' => 'applications/diffusion/query/pathid/DiffusionPathIDQuery.php',
'DiffusionPathQuery' => 'applications/diffusion/query/DiffusionPathQuery.php',
'DiffusionPathQueryTestCase' => 'applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php',
'DiffusionPathTreeController' => 'applications/diffusion/controller/DiffusionPathTreeController.php',
'DiffusionPathValidateController' => 'applications/diffusion/controller/DiffusionPathValidateController.php',
'DiffusionPatternSearchView' => 'applications/diffusion/view/DiffusionPatternSearchView.php',
'DiffusionPhpExternalSymbolsSource' => 'applications/diffusion/symbol/DiffusionPhpExternalSymbolsSource.php',
'DiffusionPreCommitContentAffectedFilesHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAffectedFilesHeraldField.php',
'DiffusionPreCommitContentAuthorHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorHeraldField.php',
'DiffusionPreCommitContentAuthorProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorProjectsHeraldField.php',
'DiffusionPreCommitContentAuthorRawHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorRawHeraldField.php',
'DiffusionPreCommitContentBranchesHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentBranchesHeraldField.php',
'DiffusionPreCommitContentCommitterHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterHeraldField.php',
'DiffusionPreCommitContentCommitterProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterProjectsHeraldField.php',
'DiffusionPreCommitContentCommitterRawHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterRawHeraldField.php',
'DiffusionPreCommitContentDiffContentAddedHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffContentAddedHeraldField.php',
'DiffusionPreCommitContentDiffContentHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffContentHeraldField.php',
'DiffusionPreCommitContentDiffContentRemovedHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffContentRemovedHeraldField.php',
'DiffusionPreCommitContentDiffEnormousHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffEnormousHeraldField.php',
'DiffusionPreCommitContentHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentHeraldField.php',
'DiffusionPreCommitContentMergeHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentMergeHeraldField.php',
'DiffusionPreCommitContentMessageHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentMessageHeraldField.php',
'DiffusionPreCommitContentPackageHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentPackageHeraldField.php',
'DiffusionPreCommitContentPackageOwnerHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentPackageOwnerHeraldField.php',
'DiffusionPreCommitContentPusherHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentPusherHeraldField.php',
'DiffusionPreCommitContentPusherIsCommitterHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentPusherIsCommitterHeraldField.php',
'DiffusionPreCommitContentPusherProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentPusherProjectsHeraldField.php',
'DiffusionPreCommitContentRepositoryHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRepositoryHeraldField.php',
'DiffusionPreCommitContentRepositoryProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRepositoryProjectsHeraldField.php',
'DiffusionPreCommitContentRevisionAcceptedHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRevisionAcceptedHeraldField.php',
'DiffusionPreCommitContentRevisionAcceptingReviewersHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRevisionAcceptingReviewersHeraldField.php',
'DiffusionPreCommitContentRevisionHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRevisionHeraldField.php',
'DiffusionPreCommitContentRevisionReviewersHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRevisionReviewersHeraldField.php',
'DiffusionPreCommitContentRevisionSubscribersHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRevisionSubscribersHeraldField.php',
'DiffusionPreCommitRefChangeHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefChangeHeraldField.php',
'DiffusionPreCommitRefHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefHeraldField.php',
'DiffusionPreCommitRefHeraldFieldGroup' => 'applications/diffusion/herald/DiffusionPreCommitRefHeraldFieldGroup.php',
'DiffusionPreCommitRefNameHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefNameHeraldField.php',
'DiffusionPreCommitRefPusherHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefPusherHeraldField.php',
'DiffusionPreCommitRefPusherProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefPusherProjectsHeraldField.php',
'DiffusionPreCommitRefRepositoryHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefRepositoryHeraldField.php',
'DiffusionPreCommitRefRepositoryProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefRepositoryProjectsHeraldField.php',
'DiffusionPreCommitRefTypeHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefTypeHeraldField.php',
'DiffusionPreCommitUsesGitLFSHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitUsesGitLFSHeraldField.php',
'DiffusionPullEventGarbageCollector' => 'applications/diffusion/garbagecollector/DiffusionPullEventGarbageCollector.php',
'DiffusionPullLogListController' => 'applications/diffusion/controller/DiffusionPullLogListController.php',
'DiffusionPullLogListView' => 'applications/diffusion/view/DiffusionPullLogListView.php',
'DiffusionPullLogSearchEngine' => 'applications/diffusion/query/DiffusionPullLogSearchEngine.php',
'DiffusionPushCapability' => 'applications/diffusion/capability/DiffusionPushCapability.php',
'DiffusionPushEventViewController' => 'applications/diffusion/controller/DiffusionPushEventViewController.php',
'DiffusionPushLogListController' => 'applications/diffusion/controller/DiffusionPushLogListController.php',
'DiffusionPushLogListView' => 'applications/diffusion/view/DiffusionPushLogListView.php',
'DiffusionPythonExternalSymbolsSource' => 'applications/diffusion/symbol/DiffusionPythonExternalSymbolsSource.php',
'DiffusionQuery' => 'applications/diffusion/query/DiffusionQuery.php',
'DiffusionQueryCommitsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionQueryCommitsConduitAPIMethod.php',
'DiffusionQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionQueryConduitAPIMethod.php',
'DiffusionQueryPathsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionQueryPathsConduitAPIMethod.php',
'DiffusionRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionRawDiffQuery.php',
'DiffusionRawDiffQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRawDiffQueryConduitAPIMethod.php',
'DiffusionReadmeView' => 'applications/diffusion/view/DiffusionReadmeView.php',
'DiffusionRefDatasource' => 'applications/diffusion/typeahead/DiffusionRefDatasource.php',
'DiffusionRefNotFoundException' => 'applications/diffusion/exception/DiffusionRefNotFoundException.php',
'DiffusionRefTableController' => 'applications/diffusion/controller/DiffusionRefTableController.php',
'DiffusionRefsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php',
'DiffusionRenameHistoryQuery' => 'applications/diffusion/query/DiffusionRenameHistoryQuery.php',
'DiffusionRepositoryActionsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryActionsManagementPanel.php',
'DiffusionRepositoryAutomationManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryAutomationManagementPanel.php',
'DiffusionRepositoryBasicsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php',
'DiffusionRepositoryBranchesManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php',
'DiffusionRepositoryByIDRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryByIDRemarkupRule.php',
'DiffusionRepositoryClusterEngine' => 'applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php',
'DiffusionRepositoryClusterEngineLogInterface' => 'applications/diffusion/protocol/DiffusionRepositoryClusterEngineLogInterface.php',
'DiffusionRepositoryController' => 'applications/diffusion/controller/DiffusionRepositoryController.php',
'DiffusionRepositoryDatasource' => 'applications/diffusion/typeahead/DiffusionRepositoryDatasource.php',
'DiffusionRepositoryDefaultController' => 'applications/diffusion/controller/DiffusionRepositoryDefaultController.php',
'DiffusionRepositoryEditActivateController' => 'applications/diffusion/controller/DiffusionRepositoryEditActivateController.php',
'DiffusionRepositoryEditConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRepositoryEditConduitAPIMethod.php',
'DiffusionRepositoryEditController' => 'applications/diffusion/controller/DiffusionRepositoryEditController.php',
'DiffusionRepositoryEditDangerousController' => 'applications/diffusion/controller/DiffusionRepositoryEditDangerousController.php',
'DiffusionRepositoryEditDeleteController' => 'applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php',
'DiffusionRepositoryEditEngine' => 'applications/diffusion/editor/DiffusionRepositoryEditEngine.php',
'DiffusionRepositoryEditEnormousController' => 'applications/diffusion/controller/DiffusionRepositoryEditEnormousController.php',
'DiffusionRepositoryEditUpdateController' => 'applications/diffusion/controller/DiffusionRepositoryEditUpdateController.php',
'DiffusionRepositoryFunctionDatasource' => 'applications/diffusion/typeahead/DiffusionRepositoryFunctionDatasource.php',
'DiffusionRepositoryHistoryManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryHistoryManagementPanel.php',
'DiffusionRepositoryIdentityEditor' => 'applications/diffusion/editor/DiffusionRepositoryIdentityEditor.php',
'DiffusionRepositoryIdentitySearchEngine' => 'applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php',
'DiffusionRepositoryLimitsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryLimitsManagementPanel.php',
'DiffusionRepositoryListController' => 'applications/diffusion/controller/DiffusionRepositoryListController.php',
'DiffusionRepositoryManageController' => 'applications/diffusion/controller/DiffusionRepositoryManageController.php',
'DiffusionRepositoryManagePanelsController' => 'applications/diffusion/controller/DiffusionRepositoryManagePanelsController.php',
'DiffusionRepositoryManagementBuildsPanelGroup' => 'applications/diffusion/management/DiffusionRepositoryManagementBuildsPanelGroup.php',
'DiffusionRepositoryManagementIntegrationsPanelGroup' => 'applications/diffusion/management/DiffusionRepositoryManagementIntegrationsPanelGroup.php',
'DiffusionRepositoryManagementMainPanelGroup' => 'applications/diffusion/management/DiffusionRepositoryManagementMainPanelGroup.php',
'DiffusionRepositoryManagementOtherPanelGroup' => 'applications/diffusion/management/DiffusionRepositoryManagementOtherPanelGroup.php',
'DiffusionRepositoryManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryManagementPanel.php',
'DiffusionRepositoryManagementPanelGroup' => 'applications/diffusion/management/DiffusionRepositoryManagementPanelGroup.php',
'DiffusionRepositoryPath' => 'applications/diffusion/data/DiffusionRepositoryPath.php',
'DiffusionRepositoryPoliciesManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryPoliciesManagementPanel.php',
'DiffusionRepositoryProfilePictureController' => 'applications/diffusion/controller/DiffusionRepositoryProfilePictureController.php',
'DiffusionRepositoryRef' => 'applications/diffusion/data/DiffusionRepositoryRef.php',
'DiffusionRepositoryRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryRemarkupRule.php',
'DiffusionRepositorySearchConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRepositorySearchConduitAPIMethod.php',
'DiffusionRepositoryStagingManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryStagingManagementPanel.php',
'DiffusionRepositoryStorageManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryStorageManagementPanel.php',
'DiffusionRepositorySubversionManagementPanel' => 'applications/diffusion/management/DiffusionRepositorySubversionManagementPanel.php',
'DiffusionRepositorySymbolsManagementPanel' => 'applications/diffusion/management/DiffusionRepositorySymbolsManagementPanel.php',
'DiffusionRepositoryTag' => 'applications/diffusion/data/DiffusionRepositoryTag.php',
'DiffusionRepositoryTestAutomationController' => 'applications/diffusion/controller/DiffusionRepositoryTestAutomationController.php',
'DiffusionRepositoryURICredentialController' => 'applications/diffusion/controller/DiffusionRepositoryURICredentialController.php',
'DiffusionRepositoryURIDisableController' => 'applications/diffusion/controller/DiffusionRepositoryURIDisableController.php',
'DiffusionRepositoryURIEditController' => 'applications/diffusion/controller/DiffusionRepositoryURIEditController.php',
'DiffusionRepositoryURIViewController' => 'applications/diffusion/controller/DiffusionRepositoryURIViewController.php',
'DiffusionRepositoryURIsIndexEngineExtension' => 'applications/diffusion/engineextension/DiffusionRepositoryURIsIndexEngineExtension.php',
'DiffusionRepositoryURIsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryURIsManagementPanel.php',
'DiffusionRepositoryURIsSearchEngineAttachment' => 'applications/diffusion/engineextension/DiffusionRepositoryURIsSearchEngineAttachment.php',
'DiffusionRequest' => 'applications/diffusion/request/DiffusionRequest.php',
'DiffusionResolveRefsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionResolveRefsConduitAPIMethod.php',
'DiffusionResolveUserQuery' => 'applications/diffusion/query/DiffusionResolveUserQuery.php',
'DiffusionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSSHWorkflow.php',
'DiffusionSearchQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php',
'DiffusionServeController' => 'applications/diffusion/controller/DiffusionServeController.php',
'DiffusionSetPasswordSettingsPanel' => 'applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php',
'DiffusionSetupException' => 'applications/diffusion/exception/DiffusionSetupException.php',
'DiffusionSubversionCommandEngine' => 'applications/diffusion/protocol/DiffusionSubversionCommandEngine.php',
'DiffusionSubversionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSubversionSSHWorkflow.php',
'DiffusionSubversionServeSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php',
'DiffusionSubversionWireProtocol' => 'applications/diffusion/protocol/DiffusionSubversionWireProtocol.php',
'DiffusionSubversionWireProtocolTestCase' => 'applications/diffusion/protocol/__tests__/DiffusionSubversionWireProtocolTestCase.php',
'DiffusionSvnBlameQuery' => 'applications/diffusion/query/blame/DiffusionSvnBlameQuery.php',
'DiffusionSvnFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionSvnFileContentQuery.php',
'DiffusionSvnRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionSvnRawDiffQuery.php',
'DiffusionSvnRequest' => 'applications/diffusion/request/DiffusionSvnRequest.php',
'DiffusionSymbolController' => 'applications/diffusion/controller/DiffusionSymbolController.php',
'DiffusionSymbolDatasource' => 'applications/diffusion/typeahead/DiffusionSymbolDatasource.php',
'DiffusionSymbolQuery' => 'applications/diffusion/query/DiffusionSymbolQuery.php',
'DiffusionSyncLogListController' => 'applications/diffusion/controller/DiffusionSyncLogListController.php',
'DiffusionSyncLogListView' => 'applications/diffusion/view/DiffusionSyncLogListView.php',
'DiffusionSyncLogSearchEngine' => 'applications/diffusion/query/DiffusionSyncLogSearchEngine.php',
'DiffusionTagListController' => 'applications/diffusion/controller/DiffusionTagListController.php',
'DiffusionTagListView' => 'applications/diffusion/view/DiffusionTagListView.php',
'DiffusionTagTableView' => 'applications/diffusion/view/DiffusionTagTableView.php',
'DiffusionTaggedRepositoriesFunctionDatasource' => 'applications/diffusion/typeahead/DiffusionTaggedRepositoriesFunctionDatasource.php',
'DiffusionTagsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionTagsQueryConduitAPIMethod.php',
'DiffusionURIEditConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionURIEditConduitAPIMethod.php',
'DiffusionURIEditEngine' => 'applications/diffusion/editor/DiffusionURIEditEngine.php',
'DiffusionURIEditor' => 'applications/diffusion/editor/DiffusionURIEditor.php',
'DiffusionURITestCase' => 'applications/diffusion/request/__tests__/DiffusionURITestCase.php',
'DiffusionUpdateCoverageConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionUpdateCoverageConduitAPIMethod.php',
'DiffusionView' => 'applications/diffusion/view/DiffusionView.php',
'DivinerArticleAtomizer' => 'applications/diviner/atomizer/DivinerArticleAtomizer.php',
'DivinerAtom' => 'applications/diviner/atom/DivinerAtom.php',
'DivinerAtomCache' => 'applications/diviner/cache/DivinerAtomCache.php',
'DivinerAtomController' => 'applications/diviner/controller/DivinerAtomController.php',
'DivinerAtomListController' => 'applications/diviner/controller/DivinerAtomListController.php',
'DivinerAtomPHIDType' => 'applications/diviner/phid/DivinerAtomPHIDType.php',
'DivinerAtomQuery' => 'applications/diviner/query/DivinerAtomQuery.php',
'DivinerAtomRef' => 'applications/diviner/atom/DivinerAtomRef.php',
'DivinerAtomSearchEngine' => 'applications/diviner/query/DivinerAtomSearchEngine.php',
'DivinerAtomizeWorkflow' => 'applications/diviner/workflow/DivinerAtomizeWorkflow.php',
'DivinerAtomizer' => 'applications/diviner/atomizer/DivinerAtomizer.php',
'DivinerBookController' => 'applications/diviner/controller/DivinerBookController.php',
'DivinerBookDatasource' => 'applications/diviner/typeahead/DivinerBookDatasource.php',
'DivinerBookEditController' => 'applications/diviner/controller/DivinerBookEditController.php',
'DivinerBookItemView' => 'applications/diviner/view/DivinerBookItemView.php',
'DivinerBookPHIDType' => 'applications/diviner/phid/DivinerBookPHIDType.php',
'DivinerBookQuery' => 'applications/diviner/query/DivinerBookQuery.php',
'DivinerController' => 'applications/diviner/controller/DivinerController.php',
'DivinerDAO' => 'applications/diviner/storage/DivinerDAO.php',
'DivinerDefaultEditCapability' => 'applications/diviner/capability/DivinerDefaultEditCapability.php',
'DivinerDefaultRenderer' => 'applications/diviner/renderer/DivinerDefaultRenderer.php',
'DivinerDefaultViewCapability' => 'applications/diviner/capability/DivinerDefaultViewCapability.php',
'DivinerDiskCache' => 'applications/diviner/cache/DivinerDiskCache.php',
'DivinerFileAtomizer' => 'applications/diviner/atomizer/DivinerFileAtomizer.php',
'DivinerFindController' => 'applications/diviner/controller/DivinerFindController.php',
'DivinerGenerateWorkflow' => 'applications/diviner/workflow/DivinerGenerateWorkflow.php',
'DivinerLiveAtom' => 'applications/diviner/storage/DivinerLiveAtom.php',
'DivinerLiveBook' => 'applications/diviner/storage/DivinerLiveBook.php',
'DivinerLiveBookEditor' => 'applications/diviner/editor/DivinerLiveBookEditor.php',
'DivinerLiveBookFulltextEngine' => 'applications/diviner/search/DivinerLiveBookFulltextEngine.php',
'DivinerLiveBookTransaction' => 'applications/diviner/storage/DivinerLiveBookTransaction.php',
'DivinerLiveBookTransactionQuery' => 'applications/diviner/query/DivinerLiveBookTransactionQuery.php',
'DivinerLivePublisher' => 'applications/diviner/publisher/DivinerLivePublisher.php',
'DivinerLiveSymbol' => 'applications/diviner/storage/DivinerLiveSymbol.php',
'DivinerLiveSymbolFulltextEngine' => 'applications/diviner/search/DivinerLiveSymbolFulltextEngine.php',
'DivinerMainController' => 'applications/diviner/controller/DivinerMainController.php',
'DivinerPHPAtomizer' => 'applications/diviner/atomizer/DivinerPHPAtomizer.php',
'DivinerParameterTableView' => 'applications/diviner/view/DivinerParameterTableView.php',
'DivinerPublishCache' => 'applications/diviner/cache/DivinerPublishCache.php',
'DivinerPublisher' => 'applications/diviner/publisher/DivinerPublisher.php',
'DivinerRenderer' => 'applications/diviner/renderer/DivinerRenderer.php',
'DivinerReturnTableView' => 'applications/diviner/view/DivinerReturnTableView.php',
'DivinerSchemaSpec' => 'applications/diviner/storage/DivinerSchemaSpec.php',
'DivinerSectionView' => 'applications/diviner/view/DivinerSectionView.php',
'DivinerStaticPublisher' => 'applications/diviner/publisher/DivinerStaticPublisher.php',
'DivinerSymbolRemarkupRule' => 'applications/diviner/markup/DivinerSymbolRemarkupRule.php',
'DivinerWorkflow' => 'applications/diviner/workflow/DivinerWorkflow.php',
'DoorkeeperAsanaFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperAsanaFeedWorker.php',
'DoorkeeperAsanaRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperAsanaRemarkupRule.php',
'DoorkeeperBridge' => 'applications/doorkeeper/bridge/DoorkeeperBridge.php',
'DoorkeeperBridgeAsana' => 'applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php',
'DoorkeeperBridgeGitHub' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHub.php',
'DoorkeeperBridgeGitHubIssue' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHubIssue.php',
'DoorkeeperBridgeGitHubUser' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHubUser.php',
'DoorkeeperBridgeJIRA' => 'applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php',
'DoorkeeperBridgeJIRATestCase' => 'applications/doorkeeper/bridge/__tests__/DoorkeeperBridgeJIRATestCase.php',
'DoorkeeperBridgedObjectCurtainExtension' => 'applications/doorkeeper/engineextension/DoorkeeperBridgedObjectCurtainExtension.php',
'DoorkeeperBridgedObjectInterface' => 'applications/doorkeeper/bridge/DoorkeeperBridgedObjectInterface.php',
'DoorkeeperDAO' => 'applications/doorkeeper/storage/DoorkeeperDAO.php',
'DoorkeeperExternalObject' => 'applications/doorkeeper/storage/DoorkeeperExternalObject.php',
'DoorkeeperExternalObjectPHIDType' => 'applications/doorkeeper/phid/DoorkeeperExternalObjectPHIDType.php',
'DoorkeeperExternalObjectQuery' => 'applications/doorkeeper/query/DoorkeeperExternalObjectQuery.php',
'DoorkeeperFeedStoryPublisher' => 'applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php',
'DoorkeeperFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperFeedWorker.php',
'DoorkeeperImportEngine' => 'applications/doorkeeper/engine/DoorkeeperImportEngine.php',
'DoorkeeperJIRAFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperJIRAFeedWorker.php',
'DoorkeeperJIRARemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperJIRARemarkupRule.php',
'DoorkeeperMissingLinkException' => 'applications/doorkeeper/exception/DoorkeeperMissingLinkException.php',
'DoorkeeperObjectRef' => 'applications/doorkeeper/engine/DoorkeeperObjectRef.php',
'DoorkeeperRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php',
'DoorkeeperSchemaSpec' => 'applications/doorkeeper/storage/DoorkeeperSchemaSpec.php',
'DoorkeeperTagView' => 'applications/doorkeeper/view/DoorkeeperTagView.php',
'DoorkeeperTagsController' => 'applications/doorkeeper/controller/DoorkeeperTagsController.php',
'DrydockAcquiredBrokenResourceException' => 'applications/drydock/exception/DrydockAcquiredBrokenResourceException.php',
'DrydockAlmanacServiceHostBlueprintImplementation' => 'applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php',
'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php',
'DrydockAuthorization' => 'applications/drydock/storage/DrydockAuthorization.php',
'DrydockAuthorizationAuthorizeController' => 'applications/drydock/controller/DrydockAuthorizationAuthorizeController.php',
'DrydockAuthorizationListController' => 'applications/drydock/controller/DrydockAuthorizationListController.php',
'DrydockAuthorizationListView' => 'applications/drydock/view/DrydockAuthorizationListView.php',
'DrydockAuthorizationPHIDType' => 'applications/drydock/phid/DrydockAuthorizationPHIDType.php',
'DrydockAuthorizationQuery' => 'applications/drydock/query/DrydockAuthorizationQuery.php',
'DrydockAuthorizationSearchConduitAPIMethod' => 'applications/drydock/conduit/DrydockAuthorizationSearchConduitAPIMethod.php',
'DrydockAuthorizationSearchEngine' => 'applications/drydock/query/DrydockAuthorizationSearchEngine.php',
'DrydockAuthorizationViewController' => 'applications/drydock/controller/DrydockAuthorizationViewController.php',
'DrydockBlueprint' => 'applications/drydock/storage/DrydockBlueprint.php',
'DrydockBlueprintController' => 'applications/drydock/controller/DrydockBlueprintController.php',
'DrydockBlueprintCoreCustomField' => 'applications/drydock/customfield/DrydockBlueprintCoreCustomField.php',
'DrydockBlueprintCustomField' => 'applications/drydock/customfield/DrydockBlueprintCustomField.php',
'DrydockBlueprintDatasource' => 'applications/drydock/typeahead/DrydockBlueprintDatasource.php',
'DrydockBlueprintDisableController' => 'applications/drydock/controller/DrydockBlueprintDisableController.php',
'DrydockBlueprintDisableTransaction' => 'applications/drydock/xaction/DrydockBlueprintDisableTransaction.php',
'DrydockBlueprintEditConduitAPIMethod' => 'applications/drydock/conduit/DrydockBlueprintEditConduitAPIMethod.php',
'DrydockBlueprintEditController' => 'applications/drydock/controller/DrydockBlueprintEditController.php',
'DrydockBlueprintEditEngine' => 'applications/drydock/editor/DrydockBlueprintEditEngine.php',
'DrydockBlueprintEditor' => 'applications/drydock/editor/DrydockBlueprintEditor.php',
'DrydockBlueprintImplementation' => 'applications/drydock/blueprint/DrydockBlueprintImplementation.php',
'DrydockBlueprintImplementationTestCase' => 'applications/drydock/blueprint/__tests__/DrydockBlueprintImplementationTestCase.php',
'DrydockBlueprintListController' => 'applications/drydock/controller/DrydockBlueprintListController.php',
'DrydockBlueprintNameNgrams' => 'applications/drydock/storage/DrydockBlueprintNameNgrams.php',
'DrydockBlueprintNameTransaction' => 'applications/drydock/xaction/DrydockBlueprintNameTransaction.php',
'DrydockBlueprintPHIDType' => 'applications/drydock/phid/DrydockBlueprintPHIDType.php',
'DrydockBlueprintQuery' => 'applications/drydock/query/DrydockBlueprintQuery.php',
'DrydockBlueprintSearchConduitAPIMethod' => 'applications/drydock/conduit/DrydockBlueprintSearchConduitAPIMethod.php',
'DrydockBlueprintSearchEngine' => 'applications/drydock/query/DrydockBlueprintSearchEngine.php',
'DrydockBlueprintTransaction' => 'applications/drydock/storage/DrydockBlueprintTransaction.php',
'DrydockBlueprintTransactionQuery' => 'applications/drydock/query/DrydockBlueprintTransactionQuery.php',
'DrydockBlueprintTransactionType' => 'applications/drydock/xaction/DrydockBlueprintTransactionType.php',
'DrydockBlueprintTypeTransaction' => 'applications/drydock/xaction/DrydockBlueprintTypeTransaction.php',
'DrydockBlueprintViewController' => 'applications/drydock/controller/DrydockBlueprintViewController.php',
'DrydockCommand' => 'applications/drydock/storage/DrydockCommand.php',
'DrydockCommandError' => 'applications/drydock/exception/DrydockCommandError.php',
'DrydockCommandInterface' => 'applications/drydock/interface/command/DrydockCommandInterface.php',
'DrydockCommandQuery' => 'applications/drydock/query/DrydockCommandQuery.php',
'DrydockConsoleController' => 'applications/drydock/controller/DrydockConsoleController.php',
'DrydockController' => 'applications/drydock/controller/DrydockController.php',
'DrydockCreateBlueprintsCapability' => 'applications/drydock/capability/DrydockCreateBlueprintsCapability.php',
'DrydockDAO' => 'applications/drydock/storage/DrydockDAO.php',
'DrydockDefaultEditCapability' => 'applications/drydock/capability/DrydockDefaultEditCapability.php',
'DrydockDefaultViewCapability' => 'applications/drydock/capability/DrydockDefaultViewCapability.php',
'DrydockFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockFilesystemInterface.php',
'DrydockInterface' => 'applications/drydock/interface/DrydockInterface.php',
'DrydockLandRepositoryOperation' => 'applications/drydock/operation/DrydockLandRepositoryOperation.php',
'DrydockLease' => 'applications/drydock/storage/DrydockLease.php',
'DrydockLeaseAcquiredLogType' => 'applications/drydock/logtype/DrydockLeaseAcquiredLogType.php',
'DrydockLeaseActivatedLogType' => 'applications/drydock/logtype/DrydockLeaseActivatedLogType.php',
'DrydockLeaseActivationFailureLogType' => 'applications/drydock/logtype/DrydockLeaseActivationFailureLogType.php',
'DrydockLeaseActivationYieldLogType' => 'applications/drydock/logtype/DrydockLeaseActivationYieldLogType.php',
'DrydockLeaseAllocationFailureLogType' => 'applications/drydock/logtype/DrydockLeaseAllocationFailureLogType.php',
'DrydockLeaseController' => 'applications/drydock/controller/DrydockLeaseController.php',
'DrydockLeaseDatasource' => 'applications/drydock/typeahead/DrydockLeaseDatasource.php',
'DrydockLeaseDestroyedLogType' => 'applications/drydock/logtype/DrydockLeaseDestroyedLogType.php',
'DrydockLeaseListController' => 'applications/drydock/controller/DrydockLeaseListController.php',
'DrydockLeaseListView' => 'applications/drydock/view/DrydockLeaseListView.php',
'DrydockLeaseNoAuthorizationsLogType' => 'applications/drydock/logtype/DrydockLeaseNoAuthorizationsLogType.php',
'DrydockLeaseNoBlueprintsLogType' => 'applications/drydock/logtype/DrydockLeaseNoBlueprintsLogType.php',
'DrydockLeasePHIDType' => 'applications/drydock/phid/DrydockLeasePHIDType.php',
'DrydockLeaseQuery' => 'applications/drydock/query/DrydockLeaseQuery.php',
'DrydockLeaseQueuedLogType' => 'applications/drydock/logtype/DrydockLeaseQueuedLogType.php',
'DrydockLeaseReacquireLogType' => 'applications/drydock/logtype/DrydockLeaseReacquireLogType.php',
'DrydockLeaseReclaimLogType' => 'applications/drydock/logtype/DrydockLeaseReclaimLogType.php',
'DrydockLeaseReleaseController' => 'applications/drydock/controller/DrydockLeaseReleaseController.php',
'DrydockLeaseReleasedLogType' => 'applications/drydock/logtype/DrydockLeaseReleasedLogType.php',
'DrydockLeaseSearchConduitAPIMethod' => 'applications/drydock/conduit/DrydockLeaseSearchConduitAPIMethod.php',
'DrydockLeaseSearchEngine' => 'applications/drydock/query/DrydockLeaseSearchEngine.php',
'DrydockLeaseStatus' => 'applications/drydock/constants/DrydockLeaseStatus.php',
'DrydockLeaseUpdateWorker' => 'applications/drydock/worker/DrydockLeaseUpdateWorker.php',
'DrydockLeaseViewController' => 'applications/drydock/controller/DrydockLeaseViewController.php',
'DrydockLeaseWaitingForResourcesLogType' => 'applications/drydock/logtype/DrydockLeaseWaitingForResourcesLogType.php',
'DrydockLog' => 'applications/drydock/storage/DrydockLog.php',
'DrydockLogController' => 'applications/drydock/controller/DrydockLogController.php',
'DrydockLogGarbageCollector' => 'applications/drydock/garbagecollector/DrydockLogGarbageCollector.php',
'DrydockLogListController' => 'applications/drydock/controller/DrydockLogListController.php',
'DrydockLogListView' => 'applications/drydock/view/DrydockLogListView.php',
'DrydockLogQuery' => 'applications/drydock/query/DrydockLogQuery.php',
'DrydockLogSearchEngine' => 'applications/drydock/query/DrydockLogSearchEngine.php',
'DrydockLogType' => 'applications/drydock/logtype/DrydockLogType.php',
'DrydockManagementCommandWorkflow' => 'applications/drydock/management/DrydockManagementCommandWorkflow.php',
'DrydockManagementLeaseWorkflow' => 'applications/drydock/management/DrydockManagementLeaseWorkflow.php',
'DrydockManagementReclaimWorkflow' => 'applications/drydock/management/DrydockManagementReclaimWorkflow.php',
'DrydockManagementReleaseLeaseWorkflow' => 'applications/drydock/management/DrydockManagementReleaseLeaseWorkflow.php',
'DrydockManagementReleaseResourceWorkflow' => 'applications/drydock/management/DrydockManagementReleaseResourceWorkflow.php',
'DrydockManagementUpdateLeaseWorkflow' => 'applications/drydock/management/DrydockManagementUpdateLeaseWorkflow.php',
'DrydockManagementUpdateResourceWorkflow' => 'applications/drydock/management/DrydockManagementUpdateResourceWorkflow.php',
'DrydockManagementWorkflow' => 'applications/drydock/management/DrydockManagementWorkflow.php',
'DrydockObjectAuthorizationView' => 'applications/drydock/view/DrydockObjectAuthorizationView.php',
'DrydockOperationWorkLogType' => 'applications/drydock/logtype/DrydockOperationWorkLogType.php',
'DrydockQuery' => 'applications/drydock/query/DrydockQuery.php',
'DrydockRepositoryOperation' => 'applications/drydock/storage/DrydockRepositoryOperation.php',
'DrydockRepositoryOperationController' => 'applications/drydock/controller/DrydockRepositoryOperationController.php',
'DrydockRepositoryOperationDismissController' => 'applications/drydock/controller/DrydockRepositoryOperationDismissController.php',
'DrydockRepositoryOperationListController' => 'applications/drydock/controller/DrydockRepositoryOperationListController.php',
'DrydockRepositoryOperationPHIDType' => 'applications/drydock/phid/DrydockRepositoryOperationPHIDType.php',
'DrydockRepositoryOperationQuery' => 'applications/drydock/query/DrydockRepositoryOperationQuery.php',
'DrydockRepositoryOperationSearchEngine' => 'applications/drydock/query/DrydockRepositoryOperationSearchEngine.php',
'DrydockRepositoryOperationStatusController' => 'applications/drydock/controller/DrydockRepositoryOperationStatusController.php',
'DrydockRepositoryOperationStatusView' => 'applications/drydock/view/DrydockRepositoryOperationStatusView.php',
'DrydockRepositoryOperationType' => 'applications/drydock/operation/DrydockRepositoryOperationType.php',
'DrydockRepositoryOperationUpdateWorker' => 'applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php',
'DrydockRepositoryOperationViewController' => 'applications/drydock/controller/DrydockRepositoryOperationViewController.php',
'DrydockResource' => 'applications/drydock/storage/DrydockResource.php',
'DrydockResourceActivationFailureLogType' => 'applications/drydock/logtype/DrydockResourceActivationFailureLogType.php',
'DrydockResourceActivationYieldLogType' => 'applications/drydock/logtype/DrydockResourceActivationYieldLogType.php',
'DrydockResourceAllocationFailureLogType' => 'applications/drydock/logtype/DrydockResourceAllocationFailureLogType.php',
'DrydockResourceController' => 'applications/drydock/controller/DrydockResourceController.php',
'DrydockResourceDatasource' => 'applications/drydock/typeahead/DrydockResourceDatasource.php',
'DrydockResourceListController' => 'applications/drydock/controller/DrydockResourceListController.php',
'DrydockResourceListView' => 'applications/drydock/view/DrydockResourceListView.php',
'DrydockResourceLockException' => 'applications/drydock/exception/DrydockResourceLockException.php',
'DrydockResourcePHIDType' => 'applications/drydock/phid/DrydockResourcePHIDType.php',
'DrydockResourceQuery' => 'applications/drydock/query/DrydockResourceQuery.php',
'DrydockResourceReclaimLogType' => 'applications/drydock/logtype/DrydockResourceReclaimLogType.php',
'DrydockResourceReleaseController' => 'applications/drydock/controller/DrydockResourceReleaseController.php',
'DrydockResourceSearchEngine' => 'applications/drydock/query/DrydockResourceSearchEngine.php',
'DrydockResourceStatus' => 'applications/drydock/constants/DrydockResourceStatus.php',
'DrydockResourceUpdateWorker' => 'applications/drydock/worker/DrydockResourceUpdateWorker.php',
'DrydockResourceViewController' => 'applications/drydock/controller/DrydockResourceViewController.php',
'DrydockSFTPFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php',
'DrydockSSHCommandInterface' => 'applications/drydock/interface/command/DrydockSSHCommandInterface.php',
'DrydockSchemaSpec' => 'applications/drydock/storage/DrydockSchemaSpec.php',
'DrydockSlotLock' => 'applications/drydock/storage/DrydockSlotLock.php',
'DrydockSlotLockException' => 'applications/drydock/exception/DrydockSlotLockException.php',
'DrydockSlotLockFailureLogType' => 'applications/drydock/logtype/DrydockSlotLockFailureLogType.php',
'DrydockTestRepositoryOperation' => 'applications/drydock/operation/DrydockTestRepositoryOperation.php',
'DrydockTextLogType' => 'applications/drydock/logtype/DrydockTextLogType.php',
'DrydockWebrootInterface' => 'applications/drydock/interface/webroot/DrydockWebrootInterface.php',
'DrydockWorker' => 'applications/drydock/worker/DrydockWorker.php',
'DrydockWorkingCopyBlueprintImplementation' => 'applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php',
'EdgeSearchConduitAPIMethod' => 'infrastructure/edges/conduit/EdgeSearchConduitAPIMethod.php',
'FeedConduitAPIMethod' => 'applications/feed/conduit/FeedConduitAPIMethod.php',
'FeedPublishConduitAPIMethod' => 'applications/feed/conduit/FeedPublishConduitAPIMethod.php',
'FeedPublisherHTTPWorker' => 'applications/feed/worker/FeedPublisherHTTPWorker.php',
'FeedPublisherWorker' => 'applications/feed/worker/FeedPublisherWorker.php',
'FeedPushWorker' => 'applications/feed/worker/FeedPushWorker.php',
'FeedQueryConduitAPIMethod' => 'applications/feed/conduit/FeedQueryConduitAPIMethod.php',
'FeedStoryNotificationGarbageCollector' => 'applications/notification/garbagecollector/FeedStoryNotificationGarbageCollector.php',
'FileAllocateConduitAPIMethod' => 'applications/files/conduit/FileAllocateConduitAPIMethod.php',
'FileConduitAPIMethod' => 'applications/files/conduit/FileConduitAPIMethod.php',
'FileCreateMailReceiver' => 'applications/files/mail/FileCreateMailReceiver.php',
'FileDeletionWorker' => 'applications/files/worker/FileDeletionWorker.php',
'FileDownloadConduitAPIMethod' => 'applications/files/conduit/FileDownloadConduitAPIMethod.php',
'FileInfoConduitAPIMethod' => 'applications/files/conduit/FileInfoConduitAPIMethod.php',
'FileMailReceiver' => 'applications/files/mail/FileMailReceiver.php',
'FileQueryChunksConduitAPIMethod' => 'applications/files/conduit/FileQueryChunksConduitAPIMethod.php',
'FileReplyHandler' => 'applications/files/mail/FileReplyHandler.php',
'FileTypeIcon' => 'applications/files/constants/FileTypeIcon.php',
'FileUploadChunkConduitAPIMethod' => 'applications/files/conduit/FileUploadChunkConduitAPIMethod.php',
'FileUploadConduitAPIMethod' => 'applications/files/conduit/FileUploadConduitAPIMethod.php',
'FileUploadHashConduitAPIMethod' => 'applications/files/conduit/FileUploadHashConduitAPIMethod.php',
'FilesDefaultViewCapability' => 'applications/files/capability/FilesDefaultViewCapability.php',
'FlagConduitAPIMethod' => 'applications/flag/conduit/FlagConduitAPIMethod.php',
'FlagDeleteConduitAPIMethod' => 'applications/flag/conduit/FlagDeleteConduitAPIMethod.php',
'FlagEditConduitAPIMethod' => 'applications/flag/conduit/FlagEditConduitAPIMethod.php',
'FlagQueryConduitAPIMethod' => 'applications/flag/conduit/FlagQueryConduitAPIMethod.php',
'FundBacker' => 'applications/fund/storage/FundBacker.php',
'FundBackerCart' => 'applications/fund/phortune/FundBackerCart.php',
'FundBackerEditor' => 'applications/fund/editor/FundBackerEditor.php',
'FundBackerListController' => 'applications/fund/controller/FundBackerListController.php',
'FundBackerPHIDType' => 'applications/fund/phid/FundBackerPHIDType.php',
'FundBackerProduct' => 'applications/fund/phortune/FundBackerProduct.php',
'FundBackerQuery' => 'applications/fund/query/FundBackerQuery.php',
'FundBackerRefundTransaction' => 'applications/fund/xaction/FundBackerRefundTransaction.php',
'FundBackerSearchEngine' => 'applications/fund/query/FundBackerSearchEngine.php',
'FundBackerStatusTransaction' => 'applications/fund/xaction/FundBackerStatusTransaction.php',
'FundBackerTransaction' => 'applications/fund/storage/FundBackerTransaction.php',
'FundBackerTransactionQuery' => 'applications/fund/query/FundBackerTransactionQuery.php',
'FundBackerTransactionType' => 'applications/fund/xaction/FundBackerTransactionType.php',
'FundController' => 'applications/fund/controller/FundController.php',
'FundCreateInitiativesCapability' => 'applications/fund/capability/FundCreateInitiativesCapability.php',
'FundDAO' => 'applications/fund/storage/FundDAO.php',
'FundDefaultViewCapability' => 'applications/fund/capability/FundDefaultViewCapability.php',
'FundInitiative' => 'applications/fund/storage/FundInitiative.php',
'FundInitiativeBackController' => 'applications/fund/controller/FundInitiativeBackController.php',
'FundInitiativeBackerTransaction' => 'applications/fund/xaction/FundInitiativeBackerTransaction.php',
'FundInitiativeCloseController' => 'applications/fund/controller/FundInitiativeCloseController.php',
'FundInitiativeDescriptionTransaction' => 'applications/fund/xaction/FundInitiativeDescriptionTransaction.php',
'FundInitiativeEditController' => 'applications/fund/controller/FundInitiativeEditController.php',
'FundInitiativeEditEngine' => 'applications/fund/editor/FundInitiativeEditEngine.php',
'FundInitiativeEditor' => 'applications/fund/editor/FundInitiativeEditor.php',
'FundInitiativeFerretEngine' => 'applications/fund/search/FundInitiativeFerretEngine.php',
'FundInitiativeFulltextEngine' => 'applications/fund/search/FundInitiativeFulltextEngine.php',
'FundInitiativeListController' => 'applications/fund/controller/FundInitiativeListController.php',
'FundInitiativeMerchantTransaction' => 'applications/fund/xaction/FundInitiativeMerchantTransaction.php',
'FundInitiativeNameTransaction' => 'applications/fund/xaction/FundInitiativeNameTransaction.php',
'FundInitiativePHIDType' => 'applications/fund/phid/FundInitiativePHIDType.php',
'FundInitiativeQuery' => 'applications/fund/query/FundInitiativeQuery.php',
'FundInitiativeRefundTransaction' => 'applications/fund/xaction/FundInitiativeRefundTransaction.php',
'FundInitiativeRemarkupRule' => 'applications/fund/remarkup/FundInitiativeRemarkupRule.php',
'FundInitiativeReplyHandler' => 'applications/fund/mail/FundInitiativeReplyHandler.php',
'FundInitiativeRisksTransaction' => 'applications/fund/xaction/FundInitiativeRisksTransaction.php',
'FundInitiativeSearchEngine' => 'applications/fund/query/FundInitiativeSearchEngine.php',
'FundInitiativeStatusTransaction' => 'applications/fund/xaction/FundInitiativeStatusTransaction.php',
'FundInitiativeTransaction' => 'applications/fund/storage/FundInitiativeTransaction.php',
'FundInitiativeTransactionComment' => 'applications/fund/storage/FundInitiativeTransactionComment.php',
'FundInitiativeTransactionQuery' => 'applications/fund/query/FundInitiativeTransactionQuery.php',
'FundInitiativeTransactionType' => 'applications/fund/xaction/FundInitiativeTransactionType.php',
'FundInitiativeViewController' => 'applications/fund/controller/FundInitiativeViewController.php',
'FundSchemaSpec' => 'applications/fund/storage/FundSchemaSpec.php',
'HarbormasterAbortOlderBuildsBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterAbortOlderBuildsBuildStepImplementation.php',
'HarbormasterArcLintBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterArcLintBuildStepImplementation.php',
'HarbormasterArcUnitBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterArcUnitBuildStepImplementation.php',
'HarbormasterArtifact' => 'applications/harbormaster/artifact/HarbormasterArtifact.php',
'HarbormasterAutotargetsTestCase' => 'applications/harbormaster/__tests__/HarbormasterAutotargetsTestCase.php',
'HarbormasterBuild' => 'applications/harbormaster/storage/build/HarbormasterBuild.php',
'HarbormasterBuildAbortedException' => 'applications/harbormaster/exception/HarbormasterBuildAbortedException.php',
'HarbormasterBuildActionController' => 'applications/harbormaster/controller/HarbormasterBuildActionController.php',
'HarbormasterBuildArcanistAutoplan' => 'applications/harbormaster/autoplan/HarbormasterBuildArcanistAutoplan.php',
'HarbormasterBuildArtifact' => 'applications/harbormaster/storage/build/HarbormasterBuildArtifact.php',
'HarbormasterBuildArtifactPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildArtifactPHIDType.php',
'HarbormasterBuildArtifactQuery' => 'applications/harbormaster/query/HarbormasterBuildArtifactQuery.php',
'HarbormasterBuildAutoplan' => 'applications/harbormaster/autoplan/HarbormasterBuildAutoplan.php',
'HarbormasterBuildCommand' => 'applications/harbormaster/storage/HarbormasterBuildCommand.php',
'HarbormasterBuildDependencyDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildDependencyDatasource.php',
'HarbormasterBuildEngine' => 'applications/harbormaster/engine/HarbormasterBuildEngine.php',
'HarbormasterBuildFailureException' => 'applications/harbormaster/exception/HarbormasterBuildFailureException.php',
'HarbormasterBuildGraph' => 'applications/harbormaster/engine/HarbormasterBuildGraph.php',
'HarbormasterBuildInitiatorDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildInitiatorDatasource.php',
'HarbormasterBuildLintMessage' => 'applications/harbormaster/storage/build/HarbormasterBuildLintMessage.php',
'HarbormasterBuildListController' => 'applications/harbormaster/controller/HarbormasterBuildListController.php',
'HarbormasterBuildLog' => 'applications/harbormaster/storage/build/HarbormasterBuildLog.php',
'HarbormasterBuildLogChunk' => 'applications/harbormaster/storage/build/HarbormasterBuildLogChunk.php',
'HarbormasterBuildLogChunkIterator' => 'applications/harbormaster/storage/build/HarbormasterBuildLogChunkIterator.php',
'HarbormasterBuildLogDownloadController' => 'applications/harbormaster/controller/HarbormasterBuildLogDownloadController.php',
'HarbormasterBuildLogPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php',
'HarbormasterBuildLogQuery' => 'applications/harbormaster/query/HarbormasterBuildLogQuery.php',
'HarbormasterBuildLogRenderController' => 'applications/harbormaster/controller/HarbormasterBuildLogRenderController.php',
'HarbormasterBuildLogSearchConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildLogSearchConduitAPIMethod.php',
'HarbormasterBuildLogSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildLogSearchEngine.php',
'HarbormasterBuildLogTestCase' => 'applications/harbormaster/__tests__/HarbormasterBuildLogTestCase.php',
'HarbormasterBuildLogView' => 'applications/harbormaster/view/HarbormasterBuildLogView.php',
'HarbormasterBuildLogViewController' => 'applications/harbormaster/controller/HarbormasterBuildLogViewController.php',
'HarbormasterBuildMessage' => 'applications/harbormaster/storage/HarbormasterBuildMessage.php',
'HarbormasterBuildMessageQuery' => 'applications/harbormaster/query/HarbormasterBuildMessageQuery.php',
'HarbormasterBuildPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPHIDType.php',
'HarbormasterBuildPlan' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php',
+ 'HarbormasterBuildPlanBehavior' => 'applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php',
+ 'HarbormasterBuildPlanBehaviorOption' => 'applications/harbormaster/plan/HarbormasterBuildPlanBehaviorOption.php',
+ 'HarbormasterBuildPlanBehaviorTransaction' => 'applications/harbormaster/xaction/plan/HarbormasterBuildPlanBehaviorTransaction.php',
'HarbormasterBuildPlanDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildPlanDatasource.php',
'HarbormasterBuildPlanDefaultEditCapability' => 'applications/harbormaster/capability/HarbormasterBuildPlanDefaultEditCapability.php',
'HarbormasterBuildPlanDefaultViewCapability' => 'applications/harbormaster/capability/HarbormasterBuildPlanDefaultViewCapability.php',
+ 'HarbormasterBuildPlanEditAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildPlanEditAPIMethod.php',
'HarbormasterBuildPlanEditEngine' => 'applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php',
'HarbormasterBuildPlanEditor' => 'applications/harbormaster/editor/HarbormasterBuildPlanEditor.php',
'HarbormasterBuildPlanNameNgrams' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlanNameNgrams.php',
+ 'HarbormasterBuildPlanNameTransaction' => 'applications/harbormaster/xaction/plan/HarbormasterBuildPlanNameTransaction.php',
'HarbormasterBuildPlanPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPlanPHIDType.php',
+ 'HarbormasterBuildPlanPolicyCodex' => 'applications/harbormaster/codex/HarbormasterBuildPlanPolicyCodex.php',
'HarbormasterBuildPlanQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanQuery.php',
'HarbormasterBuildPlanSearchAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildPlanSearchAPIMethod.php',
'HarbormasterBuildPlanSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildPlanSearchEngine.php',
+ 'HarbormasterBuildPlanStatusTransaction' => 'applications/harbormaster/xaction/plan/HarbormasterBuildPlanStatusTransaction.php',
'HarbormasterBuildPlanTransaction' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php',
'HarbormasterBuildPlanTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanTransactionQuery.php',
+ 'HarbormasterBuildPlanTransactionType' => 'applications/harbormaster/xaction/plan/HarbormasterBuildPlanTransactionType.php',
'HarbormasterBuildQuery' => 'applications/harbormaster/query/HarbormasterBuildQuery.php',
'HarbormasterBuildRequest' => 'applications/harbormaster/engine/HarbormasterBuildRequest.php',
'HarbormasterBuildSearchConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildSearchConduitAPIMethod.php',
'HarbormasterBuildSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildSearchEngine.php',
'HarbormasterBuildStatus' => 'applications/harbormaster/constants/HarbormasterBuildStatus.php',
'HarbormasterBuildStatusDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildStatusDatasource.php',
'HarbormasterBuildStep' => 'applications/harbormaster/storage/configuration/HarbormasterBuildStep.php',
'HarbormasterBuildStepCoreCustomField' => 'applications/harbormaster/customfield/HarbormasterBuildStepCoreCustomField.php',
'HarbormasterBuildStepCustomField' => 'applications/harbormaster/customfield/HarbormasterBuildStepCustomField.php',
'HarbormasterBuildStepEditor' => 'applications/harbormaster/editor/HarbormasterBuildStepEditor.php',
'HarbormasterBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterBuildStepGroup.php',
'HarbormasterBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterBuildStepImplementation.php',
'HarbormasterBuildStepImplementationTestCase' => 'applications/harbormaster/step/__tests__/HarbormasterBuildStepImplementationTestCase.php',
'HarbormasterBuildStepPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildStepPHIDType.php',
'HarbormasterBuildStepQuery' => 'applications/harbormaster/query/HarbormasterBuildStepQuery.php',
'HarbormasterBuildStepTransaction' => 'applications/harbormaster/storage/configuration/HarbormasterBuildStepTransaction.php',
'HarbormasterBuildStepTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildStepTransactionQuery.php',
'HarbormasterBuildTarget' => 'applications/harbormaster/storage/build/HarbormasterBuildTarget.php',
'HarbormasterBuildTargetPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildTargetPHIDType.php',
'HarbormasterBuildTargetQuery' => 'applications/harbormaster/query/HarbormasterBuildTargetQuery.php',
'HarbormasterBuildTargetSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildTargetSearchEngine.php',
'HarbormasterBuildTransaction' => 'applications/harbormaster/storage/HarbormasterBuildTransaction.php',
'HarbormasterBuildTransactionEditor' => 'applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php',
'HarbormasterBuildTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildTransactionQuery.php',
'HarbormasterBuildUnitMessage' => 'applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php',
+ 'HarbormasterBuildUnitMessageQuery' => 'applications/harbormaster/query/HarbormasterBuildUnitMessageQuery.php',
+ 'HarbormasterBuildView' => 'applications/harbormaster/view/HarbormasterBuildView.php',
'HarbormasterBuildViewController' => 'applications/harbormaster/controller/HarbormasterBuildViewController.php',
'HarbormasterBuildWorker' => 'applications/harbormaster/worker/HarbormasterBuildWorker.php',
'HarbormasterBuildable' => 'applications/harbormaster/storage/HarbormasterBuildable.php',
'HarbormasterBuildableActionController' => 'applications/harbormaster/controller/HarbormasterBuildableActionController.php',
'HarbormasterBuildableAdapterInterface' => 'applications/harbormaster/herald/HarbormasterBuildableAdapterInterface.php',
'HarbormasterBuildableEngine' => 'applications/harbormaster/engine/HarbormasterBuildableEngine.php',
'HarbormasterBuildableInterface' => 'applications/harbormaster/interface/HarbormasterBuildableInterface.php',
'HarbormasterBuildableListController' => 'applications/harbormaster/controller/HarbormasterBuildableListController.php',
'HarbormasterBuildablePHIDType' => 'applications/harbormaster/phid/HarbormasterBuildablePHIDType.php',
'HarbormasterBuildableQuery' => 'applications/harbormaster/query/HarbormasterBuildableQuery.php',
'HarbormasterBuildableSearchAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildableSearchAPIMethod.php',
'HarbormasterBuildableSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildableSearchEngine.php',
'HarbormasterBuildableStatus' => 'applications/harbormaster/constants/HarbormasterBuildableStatus.php',
'HarbormasterBuildableTransaction' => 'applications/harbormaster/storage/HarbormasterBuildableTransaction.php',
'HarbormasterBuildableTransactionEditor' => 'applications/harbormaster/editor/HarbormasterBuildableTransactionEditor.php',
'HarbormasterBuildableTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildableTransactionQuery.php',
'HarbormasterBuildableViewController' => 'applications/harbormaster/controller/HarbormasterBuildableViewController.php',
'HarbormasterBuildkiteBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php',
'HarbormasterBuildkiteBuildableInterface' => 'applications/harbormaster/interface/HarbormasterBuildkiteBuildableInterface.php',
'HarbormasterBuildkiteHookController' => 'applications/harbormaster/controller/HarbormasterBuildkiteHookController.php',
'HarbormasterBuiltinBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterBuiltinBuildStepGroup.php',
'HarbormasterCircleCIBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php',
'HarbormasterCircleCIBuildableInterface' => 'applications/harbormaster/interface/HarbormasterCircleCIBuildableInterface.php',
'HarbormasterCircleCIHookController' => 'applications/harbormaster/controller/HarbormasterCircleCIHookController.php',
'HarbormasterConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php',
'HarbormasterControlBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterControlBuildStepGroup.php',
'HarbormasterController' => 'applications/harbormaster/controller/HarbormasterController.php',
'HarbormasterCreateArtifactConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterCreateArtifactConduitAPIMethod.php',
'HarbormasterCreatePlansCapability' => 'applications/harbormaster/capability/HarbormasterCreatePlansCapability.php',
'HarbormasterDAO' => 'applications/harbormaster/storage/HarbormasterDAO.php',
'HarbormasterDrydockBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterDrydockBuildStepGroup.php',
'HarbormasterDrydockCommandBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterDrydockCommandBuildStepImplementation.php',
'HarbormasterDrydockLeaseArtifact' => 'applications/harbormaster/artifact/HarbormasterDrydockLeaseArtifact.php',
'HarbormasterExecFuture' => 'applications/harbormaster/future/HarbormasterExecFuture.php',
'HarbormasterExternalBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterExternalBuildStepGroup.php',
'HarbormasterFileArtifact' => 'applications/harbormaster/artifact/HarbormasterFileArtifact.php',
'HarbormasterHTTPRequestBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php',
'HarbormasterHostArtifact' => 'applications/harbormaster/artifact/HarbormasterHostArtifact.php',
'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php',
'HarbormasterLintMessagesController' => 'applications/harbormaster/controller/HarbormasterLintMessagesController.php',
'HarbormasterLintPropertyView' => 'applications/harbormaster/view/HarbormasterLintPropertyView.php',
'HarbormasterLogWorker' => 'applications/harbormaster/worker/HarbormasterLogWorker.php',
'HarbormasterManagementArchiveLogsWorkflow' => 'applications/harbormaster/management/HarbormasterManagementArchiveLogsWorkflow.php',
'HarbormasterManagementBuildWorkflow' => 'applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php',
'HarbormasterManagementPublishWorkflow' => 'applications/harbormaster/management/HarbormasterManagementPublishWorkflow.php',
'HarbormasterManagementRebuildLogWorkflow' => 'applications/harbormaster/management/HarbormasterManagementRebuildLogWorkflow.php',
'HarbormasterManagementRestartWorkflow' => 'applications/harbormaster/management/HarbormasterManagementRestartWorkflow.php',
'HarbormasterManagementUpdateWorkflow' => 'applications/harbormaster/management/HarbormasterManagementUpdateWorkflow.php',
'HarbormasterManagementWorkflow' => 'applications/harbormaster/management/HarbormasterManagementWorkflow.php',
'HarbormasterManagementWriteLogWorkflow' => 'applications/harbormaster/management/HarbormasterManagementWriteLogWorkflow.php',
'HarbormasterMessageType' => 'applications/harbormaster/engine/HarbormasterMessageType.php',
'HarbormasterObject' => 'applications/harbormaster/storage/HarbormasterObject.php',
'HarbormasterOtherBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterOtherBuildStepGroup.php',
+ 'HarbormasterPlanBehaviorController' => 'applications/harbormaster/controller/HarbormasterPlanBehaviorController.php',
'HarbormasterPlanController' => 'applications/harbormaster/controller/HarbormasterPlanController.php',
'HarbormasterPlanDisableController' => 'applications/harbormaster/controller/HarbormasterPlanDisableController.php',
'HarbormasterPlanEditController' => 'applications/harbormaster/controller/HarbormasterPlanEditController.php',
'HarbormasterPlanListController' => 'applications/harbormaster/controller/HarbormasterPlanListController.php',
'HarbormasterPlanRunController' => 'applications/harbormaster/controller/HarbormasterPlanRunController.php',
'HarbormasterPlanViewController' => 'applications/harbormaster/controller/HarbormasterPlanViewController.php',
'HarbormasterPrototypeBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterPrototypeBuildStepGroup.php',
'HarbormasterPublishFragmentBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php',
'HarbormasterQueryAutotargetsConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterQueryAutotargetsConduitAPIMethod.php',
'HarbormasterQueryBuildablesConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterQueryBuildablesConduitAPIMethod.php',
'HarbormasterQueryBuildsConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterQueryBuildsConduitAPIMethod.php',
'HarbormasterQueryBuildsSearchEngineAttachment' => 'applications/harbormaster/engineextension/HarbormasterQueryBuildsSearchEngineAttachment.php',
'HarbormasterRemarkupRule' => 'applications/harbormaster/remarkup/HarbormasterRemarkupRule.php',
+ 'HarbormasterRestartException' => 'applications/harbormaster/exception/HarbormasterRestartException.php',
'HarbormasterRunBuildPlansHeraldAction' => 'applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php',
'HarbormasterSchemaSpec' => 'applications/harbormaster/storage/HarbormasterSchemaSpec.php',
'HarbormasterScratchTable' => 'applications/harbormaster/storage/HarbormasterScratchTable.php',
'HarbormasterSendMessageConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterSendMessageConduitAPIMethod.php',
'HarbormasterSleepBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterSleepBuildStepImplementation.php',
'HarbormasterStepAddController' => 'applications/harbormaster/controller/HarbormasterStepAddController.php',
'HarbormasterStepDeleteController' => 'applications/harbormaster/controller/HarbormasterStepDeleteController.php',
'HarbormasterStepEditController' => 'applications/harbormaster/controller/HarbormasterStepEditController.php',
'HarbormasterStepViewController' => 'applications/harbormaster/controller/HarbormasterStepViewController.php',
+ 'HarbormasterString' => 'applications/harbormaster/storage/HarbormasterString.php',
'HarbormasterTargetEngine' => 'applications/harbormaster/engine/HarbormasterTargetEngine.php',
'HarbormasterTargetSearchAPIMethod' => 'applications/harbormaster/conduit/HarbormasterTargetSearchAPIMethod.php',
'HarbormasterTargetWorker' => 'applications/harbormaster/worker/HarbormasterTargetWorker.php',
'HarbormasterTestBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterTestBuildStepGroup.php',
'HarbormasterThrowExceptionBuildStep' => 'applications/harbormaster/step/HarbormasterThrowExceptionBuildStep.php',
'HarbormasterUIEventListener' => 'applications/harbormaster/event/HarbormasterUIEventListener.php',
'HarbormasterURIArtifact' => 'applications/harbormaster/artifact/HarbormasterURIArtifact.php',
'HarbormasterUnitMessageListController' => 'applications/harbormaster/controller/HarbormasterUnitMessageListController.php',
'HarbormasterUnitMessageViewController' => 'applications/harbormaster/controller/HarbormasterUnitMessageViewController.php',
'HarbormasterUnitPropertyView' => 'applications/harbormaster/view/HarbormasterUnitPropertyView.php',
'HarbormasterUnitStatus' => 'applications/harbormaster/constants/HarbormasterUnitStatus.php',
'HarbormasterUnitSummaryView' => 'applications/harbormaster/view/HarbormasterUnitSummaryView.php',
'HarbormasterUploadArtifactBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php',
'HarbormasterWaitForPreviousBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php',
'HarbormasterWorker' => 'applications/harbormaster/worker/HarbormasterWorker.php',
'HarbormasterWorkingCopyArtifact' => 'applications/harbormaster/artifact/HarbormasterWorkingCopyArtifact.php',
'HeraldActingUserField' => 'applications/herald/field/HeraldActingUserField.php',
'HeraldAction' => 'applications/herald/action/HeraldAction.php',
'HeraldActionGroup' => 'applications/herald/action/HeraldActionGroup.php',
'HeraldActionRecord' => 'applications/herald/storage/HeraldActionRecord.php',
'HeraldAdapter' => 'applications/herald/adapter/HeraldAdapter.php',
'HeraldAdapterDatasource' => 'applications/herald/typeahead/HeraldAdapterDatasource.php',
'HeraldAlwaysField' => 'applications/herald/field/HeraldAlwaysField.php',
'HeraldAnotherRuleField' => 'applications/herald/field/HeraldAnotherRuleField.php',
'HeraldApplicationActionGroup' => 'applications/herald/action/HeraldApplicationActionGroup.php',
'HeraldApplyTranscript' => 'applications/herald/storage/transcript/HeraldApplyTranscript.php',
'HeraldBasicFieldGroup' => 'applications/herald/field/HeraldBasicFieldGroup.php',
'HeraldBuildableState' => 'applications/herald/state/HeraldBuildableState.php',
'HeraldCallWebhookAction' => 'applications/herald/action/HeraldCallWebhookAction.php',
'HeraldCommentAction' => 'applications/herald/action/HeraldCommentAction.php',
'HeraldCommitAdapter' => 'applications/diffusion/herald/HeraldCommitAdapter.php',
'HeraldCondition' => 'applications/herald/storage/HeraldCondition.php',
'HeraldConditionTranscript' => 'applications/herald/storage/transcript/HeraldConditionTranscript.php',
'HeraldContentSourceField' => 'applications/herald/field/HeraldContentSourceField.php',
'HeraldController' => 'applications/herald/controller/HeraldController.php',
'HeraldCoreStateReasons' => 'applications/herald/state/HeraldCoreStateReasons.php',
'HeraldCreateWebhooksCapability' => 'applications/herald/capability/HeraldCreateWebhooksCapability.php',
'HeraldDAO' => 'applications/herald/storage/HeraldDAO.php',
'HeraldDeprecatedFieldGroup' => 'applications/herald/field/HeraldDeprecatedFieldGroup.php',
'HeraldDifferentialAdapter' => 'applications/differential/herald/HeraldDifferentialAdapter.php',
'HeraldDifferentialDiffAdapter' => 'applications/differential/herald/HeraldDifferentialDiffAdapter.php',
'HeraldDifferentialRevisionAdapter' => 'applications/differential/herald/HeraldDifferentialRevisionAdapter.php',
'HeraldDisableController' => 'applications/herald/controller/HeraldDisableController.php',
'HeraldDoNothingAction' => 'applications/herald/action/HeraldDoNothingAction.php',
'HeraldEditFieldGroup' => 'applications/herald/field/HeraldEditFieldGroup.php',
'HeraldEffect' => 'applications/herald/engine/HeraldEffect.php',
'HeraldEmptyFieldValue' => 'applications/herald/value/HeraldEmptyFieldValue.php',
'HeraldEngine' => 'applications/herald/engine/HeraldEngine.php',
'HeraldExactProjectsField' => 'applications/project/herald/HeraldExactProjectsField.php',
'HeraldField' => 'applications/herald/field/HeraldField.php',
'HeraldFieldGroup' => 'applications/herald/field/HeraldFieldGroup.php',
'HeraldFieldTestCase' => 'applications/herald/field/__tests__/HeraldFieldTestCase.php',
'HeraldFieldValue' => 'applications/herald/value/HeraldFieldValue.php',
'HeraldGroup' => 'applications/herald/group/HeraldGroup.php',
'HeraldInvalidActionException' => 'applications/herald/engine/exception/HeraldInvalidActionException.php',
'HeraldInvalidConditionException' => 'applications/herald/engine/exception/HeraldInvalidConditionException.php',
'HeraldMailableState' => 'applications/herald/state/HeraldMailableState.php',
'HeraldManageGlobalRulesCapability' => 'applications/herald/capability/HeraldManageGlobalRulesCapability.php',
'HeraldManagementWorkflow' => 'applications/herald/management/HeraldManagementWorkflow.php',
'HeraldManiphestTaskAdapter' => 'applications/maniphest/herald/HeraldManiphestTaskAdapter.php',
'HeraldNewController' => 'applications/herald/controller/HeraldNewController.php',
'HeraldNewObjectField' => 'applications/herald/field/HeraldNewObjectField.php',
'HeraldNotifyActionGroup' => 'applications/herald/action/HeraldNotifyActionGroup.php',
'HeraldObjectTranscript' => 'applications/herald/storage/transcript/HeraldObjectTranscript.php',
'HeraldPhameBlogAdapter' => 'applications/phame/herald/HeraldPhameBlogAdapter.php',
'HeraldPhamePostAdapter' => 'applications/phame/herald/HeraldPhamePostAdapter.php',
'HeraldPholioMockAdapter' => 'applications/pholio/herald/HeraldPholioMockAdapter.php',
'HeraldPonderQuestionAdapter' => 'applications/ponder/herald/HeraldPonderQuestionAdapter.php',
'HeraldPreCommitAdapter' => 'applications/diffusion/herald/HeraldPreCommitAdapter.php',
'HeraldPreCommitContentAdapter' => 'applications/diffusion/herald/HeraldPreCommitContentAdapter.php',
'HeraldPreCommitRefAdapter' => 'applications/diffusion/herald/HeraldPreCommitRefAdapter.php',
'HeraldPreventActionGroup' => 'applications/herald/action/HeraldPreventActionGroup.php',
'HeraldProjectsField' => 'applications/project/herald/HeraldProjectsField.php',
'HeraldRecursiveConditionsException' => 'applications/herald/engine/exception/HeraldRecursiveConditionsException.php',
'HeraldRelatedFieldGroup' => 'applications/herald/field/HeraldRelatedFieldGroup.php',
'HeraldRemarkupFieldValue' => 'applications/herald/value/HeraldRemarkupFieldValue.php',
'HeraldRemarkupRule' => 'applications/herald/remarkup/HeraldRemarkupRule.php',
'HeraldRule' => 'applications/herald/storage/HeraldRule.php',
+ 'HeraldRuleActionAffectsObjectEdgeType' => 'applications/herald/edge/HeraldRuleActionAffectsObjectEdgeType.php',
'HeraldRuleAdapter' => 'applications/herald/adapter/HeraldRuleAdapter.php',
'HeraldRuleAdapterField' => 'applications/herald/field/rule/HeraldRuleAdapterField.php',
'HeraldRuleController' => 'applications/herald/controller/HeraldRuleController.php',
'HeraldRuleDatasource' => 'applications/herald/typeahead/HeraldRuleDatasource.php',
+ 'HeraldRuleDisableTransaction' => 'applications/herald/xaction/HeraldRuleDisableTransaction.php',
+ 'HeraldRuleEditTransaction' => 'applications/herald/xaction/HeraldRuleEditTransaction.php',
'HeraldRuleEditor' => 'applications/herald/editor/HeraldRuleEditor.php',
'HeraldRuleField' => 'applications/herald/field/rule/HeraldRuleField.php',
'HeraldRuleFieldGroup' => 'applications/herald/field/rule/HeraldRuleFieldGroup.php',
+ 'HeraldRuleIndexEngineExtension' => 'applications/herald/engineextension/HeraldRuleIndexEngineExtension.php',
'HeraldRuleListController' => 'applications/herald/controller/HeraldRuleListController.php',
+ 'HeraldRuleListView' => 'applications/herald/view/HeraldRuleListView.php',
+ 'HeraldRuleNameTransaction' => 'applications/herald/xaction/HeraldRuleNameTransaction.php',
'HeraldRulePHIDType' => 'applications/herald/phid/HeraldRulePHIDType.php',
'HeraldRuleQuery' => 'applications/herald/query/HeraldRuleQuery.php',
'HeraldRuleReplyHandler' => 'applications/herald/mail/HeraldRuleReplyHandler.php',
'HeraldRuleSearchEngine' => 'applications/herald/query/HeraldRuleSearchEngine.php',
'HeraldRuleSerializer' => 'applications/herald/editor/HeraldRuleSerializer.php',
'HeraldRuleTestCase' => 'applications/herald/storage/__tests__/HeraldRuleTestCase.php',
'HeraldRuleTransaction' => 'applications/herald/storage/HeraldRuleTransaction.php',
- 'HeraldRuleTransactionComment' => 'applications/herald/storage/HeraldRuleTransactionComment.php',
+ 'HeraldRuleTransactionType' => 'applications/herald/xaction/HeraldRuleTransactionType.php',
'HeraldRuleTranscript' => 'applications/herald/storage/transcript/HeraldRuleTranscript.php',
'HeraldRuleTypeConfig' => 'applications/herald/config/HeraldRuleTypeConfig.php',
'HeraldRuleTypeDatasource' => 'applications/herald/typeahead/HeraldRuleTypeDatasource.php',
'HeraldRuleTypeField' => 'applications/herald/field/rule/HeraldRuleTypeField.php',
'HeraldRuleViewController' => 'applications/herald/controller/HeraldRuleViewController.php',
'HeraldSchemaSpec' => 'applications/herald/storage/HeraldSchemaSpec.php',
'HeraldSelectFieldValue' => 'applications/herald/value/HeraldSelectFieldValue.php',
'HeraldSpaceField' => 'applications/spaces/herald/HeraldSpaceField.php',
'HeraldState' => 'applications/herald/state/HeraldState.php',
'HeraldStateReasons' => 'applications/herald/state/HeraldStateReasons.php',
'HeraldSubscribersField' => 'applications/subscriptions/herald/HeraldSubscribersField.php',
'HeraldSupportActionGroup' => 'applications/herald/action/HeraldSupportActionGroup.php',
'HeraldSupportFieldGroup' => 'applications/herald/field/HeraldSupportFieldGroup.php',
'HeraldTestConsoleController' => 'applications/herald/controller/HeraldTestConsoleController.php',
'HeraldTestManagementWorkflow' => 'applications/herald/management/HeraldTestManagementWorkflow.php',
'HeraldTextFieldValue' => 'applications/herald/value/HeraldTextFieldValue.php',
'HeraldTokenizerFieldValue' => 'applications/herald/value/HeraldTokenizerFieldValue.php',
'HeraldTransactionQuery' => 'applications/herald/query/HeraldTransactionQuery.php',
'HeraldTranscript' => 'applications/herald/storage/transcript/HeraldTranscript.php',
'HeraldTranscriptController' => 'applications/herald/controller/HeraldTranscriptController.php',
'HeraldTranscriptDestructionEngineExtension' => 'applications/herald/engineextension/HeraldTranscriptDestructionEngineExtension.php',
'HeraldTranscriptGarbageCollector' => 'applications/herald/garbagecollector/HeraldTranscriptGarbageCollector.php',
'HeraldTranscriptListController' => 'applications/herald/controller/HeraldTranscriptListController.php',
'HeraldTranscriptPHIDType' => 'applications/herald/phid/HeraldTranscriptPHIDType.php',
'HeraldTranscriptQuery' => 'applications/herald/query/HeraldTranscriptQuery.php',
'HeraldTranscriptSearchEngine' => 'applications/herald/query/HeraldTranscriptSearchEngine.php',
'HeraldTranscriptTestCase' => 'applications/herald/storage/__tests__/HeraldTranscriptTestCase.php',
'HeraldUtilityActionGroup' => 'applications/herald/action/HeraldUtilityActionGroup.php',
'HeraldWebhook' => 'applications/herald/storage/HeraldWebhook.php',
'HeraldWebhookCallManagementWorkflow' => 'applications/herald/management/HeraldWebhookCallManagementWorkflow.php',
'HeraldWebhookController' => 'applications/herald/controller/HeraldWebhookController.php',
'HeraldWebhookDatasource' => 'applications/herald/typeahead/HeraldWebhookDatasource.php',
'HeraldWebhookEditController' => 'applications/herald/controller/HeraldWebhookEditController.php',
'HeraldWebhookEditEngine' => 'applications/herald/editor/HeraldWebhookEditEngine.php',
'HeraldWebhookEditor' => 'applications/herald/editor/HeraldWebhookEditor.php',
'HeraldWebhookKeyController' => 'applications/herald/controller/HeraldWebhookKeyController.php',
'HeraldWebhookListController' => 'applications/herald/controller/HeraldWebhookListController.php',
'HeraldWebhookManagementWorkflow' => 'applications/herald/management/HeraldWebhookManagementWorkflow.php',
'HeraldWebhookNameTransaction' => 'applications/herald/xaction/HeraldWebhookNameTransaction.php',
'HeraldWebhookPHIDType' => 'applications/herald/phid/HeraldWebhookPHIDType.php',
'HeraldWebhookQuery' => 'applications/herald/query/HeraldWebhookQuery.php',
'HeraldWebhookRequest' => 'applications/herald/storage/HeraldWebhookRequest.php',
'HeraldWebhookRequestGarbageCollector' => 'applications/herald/garbagecollector/HeraldWebhookRequestGarbageCollector.php',
'HeraldWebhookRequestListView' => 'applications/herald/view/HeraldWebhookRequestListView.php',
'HeraldWebhookRequestPHIDType' => 'applications/herald/phid/HeraldWebhookRequestPHIDType.php',
'HeraldWebhookRequestQuery' => 'applications/herald/query/HeraldWebhookRequestQuery.php',
'HeraldWebhookSearchEngine' => 'applications/herald/query/HeraldWebhookSearchEngine.php',
'HeraldWebhookStatusTransaction' => 'applications/herald/xaction/HeraldWebhookStatusTransaction.php',
'HeraldWebhookTestController' => 'applications/herald/controller/HeraldWebhookTestController.php',
'HeraldWebhookTransaction' => 'applications/herald/storage/HeraldWebhookTransaction.php',
'HeraldWebhookTransactionQuery' => 'applications/herald/query/HeraldWebhookTransactionQuery.php',
'HeraldWebhookTransactionType' => 'applications/herald/xaction/HeraldWebhookTransactionType.php',
'HeraldWebhookURITransaction' => 'applications/herald/xaction/HeraldWebhookURITransaction.php',
'HeraldWebhookViewController' => 'applications/herald/controller/HeraldWebhookViewController.php',
'HeraldWebhookWorker' => 'applications/herald/worker/HeraldWebhookWorker.php',
'Javelin' => 'infrastructure/javelin/Javelin.php',
'LegalpadController' => 'applications/legalpad/controller/LegalpadController.php',
'LegalpadCreateDocumentsCapability' => 'applications/legalpad/capability/LegalpadCreateDocumentsCapability.php',
'LegalpadDAO' => 'applications/legalpad/storage/LegalpadDAO.php',
'LegalpadDefaultEditCapability' => 'applications/legalpad/capability/LegalpadDefaultEditCapability.php',
'LegalpadDefaultViewCapability' => 'applications/legalpad/capability/LegalpadDefaultViewCapability.php',
'LegalpadDocument' => 'applications/legalpad/storage/LegalpadDocument.php',
'LegalpadDocumentBody' => 'applications/legalpad/storage/LegalpadDocumentBody.php',
'LegalpadDocumentDatasource' => 'applications/legalpad/typeahead/LegalpadDocumentDatasource.php',
'LegalpadDocumentDoneController' => 'applications/legalpad/controller/LegalpadDocumentDoneController.php',
'LegalpadDocumentEditController' => 'applications/legalpad/controller/LegalpadDocumentEditController.php',
'LegalpadDocumentEditEngine' => 'applications/legalpad/editor/LegalpadDocumentEditEngine.php',
'LegalpadDocumentEditor' => 'applications/legalpad/editor/LegalpadDocumentEditor.php',
'LegalpadDocumentListController' => 'applications/legalpad/controller/LegalpadDocumentListController.php',
'LegalpadDocumentManageController' => 'applications/legalpad/controller/LegalpadDocumentManageController.php',
'LegalpadDocumentPreambleTransaction' => 'applications/legalpad/xaction/LegalpadDocumentPreambleTransaction.php',
'LegalpadDocumentQuery' => 'applications/legalpad/query/LegalpadDocumentQuery.php',
'LegalpadDocumentRemarkupRule' => 'applications/legalpad/remarkup/LegalpadDocumentRemarkupRule.php',
'LegalpadDocumentRequireSignatureTransaction' => 'applications/legalpad/xaction/LegalpadDocumentRequireSignatureTransaction.php',
'LegalpadDocumentSearchEngine' => 'applications/legalpad/query/LegalpadDocumentSearchEngine.php',
'LegalpadDocumentSignController' => 'applications/legalpad/controller/LegalpadDocumentSignController.php',
'LegalpadDocumentSignature' => 'applications/legalpad/storage/LegalpadDocumentSignature.php',
'LegalpadDocumentSignatureAddController' => 'applications/legalpad/controller/LegalpadDocumentSignatureAddController.php',
'LegalpadDocumentSignatureListController' => 'applications/legalpad/controller/LegalpadDocumentSignatureListController.php',
'LegalpadDocumentSignatureQuery' => 'applications/legalpad/query/LegalpadDocumentSignatureQuery.php',
'LegalpadDocumentSignatureSearchEngine' => 'applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php',
'LegalpadDocumentSignatureTypeTransaction' => 'applications/legalpad/xaction/LegalpadDocumentSignatureTypeTransaction.php',
'LegalpadDocumentSignatureVerificationController' => 'applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php',
'LegalpadDocumentSignatureViewController' => 'applications/legalpad/controller/LegalpadDocumentSignatureViewController.php',
'LegalpadDocumentTextTransaction' => 'applications/legalpad/xaction/LegalpadDocumentTextTransaction.php',
'LegalpadDocumentTitleTransaction' => 'applications/legalpad/xaction/LegalpadDocumentTitleTransaction.php',
'LegalpadDocumentTransactionType' => 'applications/legalpad/xaction/LegalpadDocumentTransactionType.php',
'LegalpadMailReceiver' => 'applications/legalpad/mail/LegalpadMailReceiver.php',
'LegalpadObjectNeedsSignatureEdgeType' => 'applications/legalpad/edge/LegalpadObjectNeedsSignatureEdgeType.php',
'LegalpadReplyHandler' => 'applications/legalpad/mail/LegalpadReplyHandler.php',
'LegalpadRequireSignatureHeraldAction' => 'applications/legalpad/herald/LegalpadRequireSignatureHeraldAction.php',
'LegalpadSchemaSpec' => 'applications/legalpad/storage/LegalpadSchemaSpec.php',
'LegalpadSignatureNeededByObjectEdgeType' => 'applications/legalpad/edge/LegalpadSignatureNeededByObjectEdgeType.php',
'LegalpadTransaction' => 'applications/legalpad/storage/LegalpadTransaction.php',
'LegalpadTransactionComment' => 'applications/legalpad/storage/LegalpadTransactionComment.php',
'LegalpadTransactionQuery' => 'applications/legalpad/query/LegalpadTransactionQuery.php',
'LiskChunkTestCase' => 'infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php',
'LiskDAO' => 'infrastructure/storage/lisk/LiskDAO.php',
'LiskDAOTestCase' => 'infrastructure/storage/lisk/__tests__/LiskDAOTestCase.php',
'LiskEphemeralObjectException' => 'infrastructure/storage/lisk/LiskEphemeralObjectException.php',
'LiskFixtureTestCase' => 'infrastructure/storage/lisk/__tests__/LiskFixtureTestCase.php',
'LiskIsolationTestCase' => 'infrastructure/storage/lisk/__tests__/LiskIsolationTestCase.php',
'LiskIsolationTestDAO' => 'infrastructure/storage/lisk/__tests__/LiskIsolationTestDAO.php',
'LiskIsolationTestDAOException' => 'infrastructure/storage/lisk/__tests__/LiskIsolationTestDAOException.php',
'LiskMigrationIterator' => 'infrastructure/storage/lisk/LiskMigrationIterator.php',
'LiskRawMigrationIterator' => 'infrastructure/storage/lisk/LiskRawMigrationIterator.php',
'MacroConduitAPIMethod' => 'applications/macro/conduit/MacroConduitAPIMethod.php',
'MacroCreateMemeConduitAPIMethod' => 'applications/macro/conduit/MacroCreateMemeConduitAPIMethod.php',
'MacroEditConduitAPIMethod' => 'applications/macro/conduit/MacroEditConduitAPIMethod.php',
'MacroEmojiExample' => 'applications/uiexample/examples/MacroEmojiExample.php',
'MacroQueryConduitAPIMethod' => 'applications/macro/conduit/MacroQueryConduitAPIMethod.php',
'ManiphestAssignEmailCommand' => 'applications/maniphest/command/ManiphestAssignEmailCommand.php',
'ManiphestAssigneeDatasource' => 'applications/maniphest/typeahead/ManiphestAssigneeDatasource.php',
'ManiphestBulkEditCapability' => 'applications/maniphest/capability/ManiphestBulkEditCapability.php',
'ManiphestBulkEditController' => 'applications/maniphest/controller/ManiphestBulkEditController.php',
'ManiphestClaimEmailCommand' => 'applications/maniphest/command/ManiphestClaimEmailCommand.php',
'ManiphestCloseEmailCommand' => 'applications/maniphest/command/ManiphestCloseEmailCommand.php',
'ManiphestConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestConduitAPIMethod.php',
'ManiphestConfiguredCustomField' => 'applications/maniphest/field/ManiphestConfiguredCustomField.php',
'ManiphestConstants' => 'applications/maniphest/constants/ManiphestConstants.php',
'ManiphestController' => 'applications/maniphest/controller/ManiphestController.php',
'ManiphestCreateMailReceiver' => 'applications/maniphest/mail/ManiphestCreateMailReceiver.php',
'ManiphestCreateTaskConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestCreateTaskConduitAPIMethod.php',
'ManiphestCustomField' => 'applications/maniphest/field/ManiphestCustomField.php',
'ManiphestCustomFieldNumericIndex' => 'applications/maniphest/storage/ManiphestCustomFieldNumericIndex.php',
'ManiphestCustomFieldStatusParser' => 'applications/maniphest/field/parser/ManiphestCustomFieldStatusParser.php',
'ManiphestCustomFieldStatusParserTestCase' => 'applications/maniphest/field/parser/__tests__/ManiphestCustomFieldStatusParserTestCase.php',
'ManiphestCustomFieldStorage' => 'applications/maniphest/storage/ManiphestCustomFieldStorage.php',
'ManiphestCustomFieldStringIndex' => 'applications/maniphest/storage/ManiphestCustomFieldStringIndex.php',
'ManiphestDAO' => 'applications/maniphest/storage/ManiphestDAO.php',
'ManiphestDefaultEditCapability' => 'applications/maniphest/capability/ManiphestDefaultEditCapability.php',
'ManiphestDefaultViewCapability' => 'applications/maniphest/capability/ManiphestDefaultViewCapability.php',
'ManiphestEditConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestEditConduitAPIMethod.php',
'ManiphestEditEngine' => 'applications/maniphest/editor/ManiphestEditEngine.php',
'ManiphestEmailCommand' => 'applications/maniphest/command/ManiphestEmailCommand.php',
'ManiphestGetTaskTransactionsConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php',
'ManiphestHovercardEngineExtension' => 'applications/maniphest/engineextension/ManiphestHovercardEngineExtension.php',
'ManiphestInfoConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestInfoConduitAPIMethod.php',
'ManiphestMailEngineExtension' => 'applications/maniphest/engineextension/ManiphestMailEngineExtension.php',
'ManiphestNameIndex' => 'applications/maniphest/storage/ManiphestNameIndex.php',
'ManiphestPointsConfigType' => 'applications/maniphest/config/ManiphestPointsConfigType.php',
'ManiphestPrioritiesConfigType' => 'applications/maniphest/config/ManiphestPrioritiesConfigType.php',
'ManiphestPriorityEmailCommand' => 'applications/maniphest/command/ManiphestPriorityEmailCommand.php',
'ManiphestPrioritySearchConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestPrioritySearchConduitAPIMethod.php',
'ManiphestProjectNameFulltextEngineExtension' => 'applications/maniphest/engineextension/ManiphestProjectNameFulltextEngineExtension.php',
'ManiphestQueryConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestQueryConduitAPIMethod.php',
'ManiphestQueryStatusesConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestQueryStatusesConduitAPIMethod.php',
'ManiphestRemarkupRule' => 'applications/maniphest/remarkup/ManiphestRemarkupRule.php',
'ManiphestReplyHandler' => 'applications/maniphest/mail/ManiphestReplyHandler.php',
'ManiphestReportController' => 'applications/maniphest/controller/ManiphestReportController.php',
'ManiphestSchemaSpec' => 'applications/maniphest/storage/ManiphestSchemaSpec.php',
'ManiphestSearchConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestSearchConduitAPIMethod.php',
'ManiphestStatusEmailCommand' => 'applications/maniphest/command/ManiphestStatusEmailCommand.php',
'ManiphestStatusSearchConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestStatusSearchConduitAPIMethod.php',
'ManiphestStatusesConfigType' => 'applications/maniphest/config/ManiphestStatusesConfigType.php',
- 'ManiphestSubpriorityController' => 'applications/maniphest/controller/ManiphestSubpriorityController.php',
'ManiphestSubtypesConfigType' => 'applications/maniphest/config/ManiphestSubtypesConfigType.php',
'ManiphestTask' => 'applications/maniphest/storage/ManiphestTask.php',
'ManiphestTaskAssignHeraldAction' => 'applications/maniphest/herald/ManiphestTaskAssignHeraldAction.php',
'ManiphestTaskAssignOtherHeraldAction' => 'applications/maniphest/herald/ManiphestTaskAssignOtherHeraldAction.php',
'ManiphestTaskAssignSelfHeraldAction' => 'applications/maniphest/herald/ManiphestTaskAssignSelfHeraldAction.php',
'ManiphestTaskAssigneeHeraldField' => 'applications/maniphest/herald/ManiphestTaskAssigneeHeraldField.php',
'ManiphestTaskAttachTransaction' => 'applications/maniphest/xaction/ManiphestTaskAttachTransaction.php',
'ManiphestTaskAuthorHeraldField' => 'applications/maniphest/herald/ManiphestTaskAuthorHeraldField.php',
'ManiphestTaskAuthorPolicyRule' => 'applications/maniphest/policyrule/ManiphestTaskAuthorPolicyRule.php',
'ManiphestTaskBulkEngine' => 'applications/maniphest/bulk/ManiphestTaskBulkEngine.php',
'ManiphestTaskCloseAsDuplicateRelationship' => 'applications/maniphest/relationship/ManiphestTaskCloseAsDuplicateRelationship.php',
'ManiphestTaskClosedStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskClosedStatusDatasource.php',
'ManiphestTaskCoverImageTransaction' => 'applications/maniphest/xaction/ManiphestTaskCoverImageTransaction.php',
'ManiphestTaskDependedOnByTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependedOnByTaskEdgeType.php',
'ManiphestTaskDependsOnTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependsOnTaskEdgeType.php',
'ManiphestTaskDescriptionHeraldField' => 'applications/maniphest/herald/ManiphestTaskDescriptionHeraldField.php',
'ManiphestTaskDescriptionTransaction' => 'applications/maniphest/xaction/ManiphestTaskDescriptionTransaction.php',
'ManiphestTaskDetailController' => 'applications/maniphest/controller/ManiphestTaskDetailController.php',
'ManiphestTaskEdgeTransaction' => 'applications/maniphest/xaction/ManiphestTaskEdgeTransaction.php',
'ManiphestTaskEditController' => 'applications/maniphest/controller/ManiphestTaskEditController.php',
'ManiphestTaskEditEngineLock' => 'applications/maniphest/editor/ManiphestTaskEditEngineLock.php',
'ManiphestTaskFerretEngine' => 'applications/maniphest/search/ManiphestTaskFerretEngine.php',
'ManiphestTaskFulltextEngine' => 'applications/maniphest/search/ManiphestTaskFulltextEngine.php',
'ManiphestTaskGraph' => 'infrastructure/graph/ManiphestTaskGraph.php',
+ 'ManiphestTaskGraphController' => 'applications/maniphest/controller/ManiphestTaskGraphController.php',
'ManiphestTaskHasCommitEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasCommitEdgeType.php',
'ManiphestTaskHasCommitRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasCommitRelationship.php',
'ManiphestTaskHasDuplicateTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasDuplicateTaskEdgeType.php',
'ManiphestTaskHasMockEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasMockEdgeType.php',
'ManiphestTaskHasMockRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasMockRelationship.php',
'ManiphestTaskHasParentRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasParentRelationship.php',
'ManiphestTaskHasRevisionEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasRevisionEdgeType.php',
'ManiphestTaskHasRevisionRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasRevisionRelationship.php',
'ManiphestTaskHasSubtaskRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasSubtaskRelationship.php',
'ManiphestTaskHeraldField' => 'applications/maniphest/herald/ManiphestTaskHeraldField.php',
'ManiphestTaskHeraldFieldGroup' => 'applications/maniphest/herald/ManiphestTaskHeraldFieldGroup.php',
'ManiphestTaskIsDuplicateOfTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskIsDuplicateOfTaskEdgeType.php',
'ManiphestTaskListController' => 'applications/maniphest/controller/ManiphestTaskListController.php',
'ManiphestTaskListHTTPParameterType' => 'applications/maniphest/httpparametertype/ManiphestTaskListHTTPParameterType.php',
'ManiphestTaskListView' => 'applications/maniphest/view/ManiphestTaskListView.php',
'ManiphestTaskMFAEngine' => 'applications/maniphest/engine/ManiphestTaskMFAEngine.php',
'ManiphestTaskMailReceiver' => 'applications/maniphest/mail/ManiphestTaskMailReceiver.php',
'ManiphestTaskMergeInRelationship' => 'applications/maniphest/relationship/ManiphestTaskMergeInRelationship.php',
'ManiphestTaskMergedFromTransaction' => 'applications/maniphest/xaction/ManiphestTaskMergedFromTransaction.php',
'ManiphestTaskMergedIntoTransaction' => 'applications/maniphest/xaction/ManiphestTaskMergedIntoTransaction.php',
'ManiphestTaskOpenStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskOpenStatusDatasource.php',
'ManiphestTaskOwnerTransaction' => 'applications/maniphest/xaction/ManiphestTaskOwnerTransaction.php',
'ManiphestTaskPHIDResolver' => 'applications/maniphest/httpparametertype/ManiphestTaskPHIDResolver.php',
'ManiphestTaskPHIDType' => 'applications/maniphest/phid/ManiphestTaskPHIDType.php',
'ManiphestTaskParentTransaction' => 'applications/maniphest/xaction/ManiphestTaskParentTransaction.php',
'ManiphestTaskPoints' => 'applications/maniphest/constants/ManiphestTaskPoints.php',
'ManiphestTaskPointsTransaction' => 'applications/maniphest/xaction/ManiphestTaskPointsTransaction.php',
+ 'ManiphestTaskPolicyCodex' => 'applications/maniphest/policy/ManiphestTaskPolicyCodex.php',
'ManiphestTaskPriority' => 'applications/maniphest/constants/ManiphestTaskPriority.php',
'ManiphestTaskPriorityDatasource' => 'applications/maniphest/typeahead/ManiphestTaskPriorityDatasource.php',
'ManiphestTaskPriorityHeraldAction' => 'applications/maniphest/herald/ManiphestTaskPriorityHeraldAction.php',
'ManiphestTaskPriorityHeraldField' => 'applications/maniphest/herald/ManiphestTaskPriorityHeraldField.php',
'ManiphestTaskPriorityTransaction' => 'applications/maniphest/xaction/ManiphestTaskPriorityTransaction.php',
'ManiphestTaskQuery' => 'applications/maniphest/query/ManiphestTaskQuery.php',
'ManiphestTaskRelationship' => 'applications/maniphest/relationship/ManiphestTaskRelationship.php',
'ManiphestTaskRelationshipSource' => 'applications/search/relationship/ManiphestTaskRelationshipSource.php',
'ManiphestTaskResultListView' => 'applications/maniphest/view/ManiphestTaskResultListView.php',
'ManiphestTaskSearchEngine' => 'applications/maniphest/query/ManiphestTaskSearchEngine.php',
'ManiphestTaskStatus' => 'applications/maniphest/constants/ManiphestTaskStatus.php',
'ManiphestTaskStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskStatusDatasource.php',
'ManiphestTaskStatusFunctionDatasource' => 'applications/maniphest/typeahead/ManiphestTaskStatusFunctionDatasource.php',
'ManiphestTaskStatusHeraldAction' => 'applications/maniphest/herald/ManiphestTaskStatusHeraldAction.php',
'ManiphestTaskStatusHeraldField' => 'applications/maniphest/herald/ManiphestTaskStatusHeraldField.php',
'ManiphestTaskStatusTestCase' => 'applications/maniphest/constants/__tests__/ManiphestTaskStatusTestCase.php',
'ManiphestTaskStatusTransaction' => 'applications/maniphest/xaction/ManiphestTaskStatusTransaction.php',
'ManiphestTaskSubpriorityTransaction' => 'applications/maniphest/xaction/ManiphestTaskSubpriorityTransaction.php',
'ManiphestTaskSubtaskController' => 'applications/maniphest/controller/ManiphestTaskSubtaskController.php',
'ManiphestTaskSubtypeDatasource' => 'applications/maniphest/typeahead/ManiphestTaskSubtypeDatasource.php',
- 'ManiphestTaskTestCase' => 'applications/maniphest/__tests__/ManiphestTaskTestCase.php',
'ManiphestTaskTitleHeraldField' => 'applications/maniphest/herald/ManiphestTaskTitleHeraldField.php',
'ManiphestTaskTitleTransaction' => 'applications/maniphest/xaction/ManiphestTaskTitleTransaction.php',
'ManiphestTaskTransactionType' => 'applications/maniphest/xaction/ManiphestTaskTransactionType.php',
'ManiphestTaskUnblockTransaction' => 'applications/maniphest/xaction/ManiphestTaskUnblockTransaction.php',
+ 'ManiphestTaskUnlockEngine' => 'applications/maniphest/engine/ManiphestTaskUnlockEngine.php',
'ManiphestTransaction' => 'applications/maniphest/storage/ManiphestTransaction.php',
'ManiphestTransactionComment' => 'applications/maniphest/storage/ManiphestTransactionComment.php',
'ManiphestTransactionEditor' => 'applications/maniphest/editor/ManiphestTransactionEditor.php',
'ManiphestTransactionQuery' => 'applications/maniphest/query/ManiphestTransactionQuery.php',
'ManiphestUpdateConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestUpdateConduitAPIMethod.php',
'ManiphestView' => 'applications/maniphest/view/ManiphestView.php',
'MetaMTAEmailTransactionCommand' => 'applications/metamta/command/MetaMTAEmailTransactionCommand.php',
'MetaMTAEmailTransactionCommandTestCase' => 'applications/metamta/command/__tests__/MetaMTAEmailTransactionCommandTestCase.php',
'MetaMTAMailReceivedGarbageCollector' => 'applications/metamta/garbagecollector/MetaMTAMailReceivedGarbageCollector.php',
'MetaMTAMailSentGarbageCollector' => 'applications/metamta/garbagecollector/MetaMTAMailSentGarbageCollector.php',
'MetaMTAReceivedMailStatus' => 'applications/metamta/constants/MetaMTAReceivedMailStatus.php',
'MultimeterContext' => 'applications/multimeter/storage/MultimeterContext.php',
'MultimeterControl' => 'applications/multimeter/data/MultimeterControl.php',
'MultimeterController' => 'applications/multimeter/controller/MultimeterController.php',
'MultimeterDAO' => 'applications/multimeter/storage/MultimeterDAO.php',
'MultimeterDimension' => 'applications/multimeter/storage/MultimeterDimension.php',
'MultimeterEvent' => 'applications/multimeter/storage/MultimeterEvent.php',
'MultimeterEventGarbageCollector' => 'applications/multimeter/garbagecollector/MultimeterEventGarbageCollector.php',
'MultimeterHost' => 'applications/multimeter/storage/MultimeterHost.php',
'MultimeterLabel' => 'applications/multimeter/storage/MultimeterLabel.php',
'MultimeterSampleController' => 'applications/multimeter/controller/MultimeterSampleController.php',
'MultimeterViewer' => 'applications/multimeter/storage/MultimeterViewer.php',
'NuanceCommandImplementation' => 'applications/nuance/command/NuanceCommandImplementation.php',
'NuanceConduitAPIMethod' => 'applications/nuance/conduit/NuanceConduitAPIMethod.php',
'NuanceConsoleController' => 'applications/nuance/controller/NuanceConsoleController.php',
'NuanceContentSource' => 'applications/nuance/contentsource/NuanceContentSource.php',
'NuanceController' => 'applications/nuance/controller/NuanceController.php',
'NuanceDAO' => 'applications/nuance/storage/NuanceDAO.php',
'NuanceFormItemType' => 'applications/nuance/item/NuanceFormItemType.php',
'NuanceGitHubEventItemType' => 'applications/nuance/item/NuanceGitHubEventItemType.php',
'NuanceGitHubImportCursor' => 'applications/nuance/cursor/NuanceGitHubImportCursor.php',
'NuanceGitHubIssuesImportCursor' => 'applications/nuance/cursor/NuanceGitHubIssuesImportCursor.php',
'NuanceGitHubRawEvent' => 'applications/nuance/github/NuanceGitHubRawEvent.php',
'NuanceGitHubRawEventTestCase' => 'applications/nuance/github/__tests__/NuanceGitHubRawEventTestCase.php',
'NuanceGitHubRepositoryImportCursor' => 'applications/nuance/cursor/NuanceGitHubRepositoryImportCursor.php',
'NuanceGitHubRepositorySourceDefinition' => 'applications/nuance/source/NuanceGitHubRepositorySourceDefinition.php',
'NuanceImportCursor' => 'applications/nuance/cursor/NuanceImportCursor.php',
'NuanceImportCursorData' => 'applications/nuance/storage/NuanceImportCursorData.php',
'NuanceImportCursorDataQuery' => 'applications/nuance/query/NuanceImportCursorDataQuery.php',
'NuanceImportCursorPHIDType' => 'applications/nuance/phid/NuanceImportCursorPHIDType.php',
'NuanceItem' => 'applications/nuance/storage/NuanceItem.php',
'NuanceItemActionController' => 'applications/nuance/controller/NuanceItemActionController.php',
'NuanceItemCommand' => 'applications/nuance/storage/NuanceItemCommand.php',
'NuanceItemCommandQuery' => 'applications/nuance/query/NuanceItemCommandQuery.php',
'NuanceItemCommandSpec' => 'applications/nuance/command/NuanceItemCommandSpec.php',
'NuanceItemCommandTransaction' => 'applications/nuance/xaction/NuanceItemCommandTransaction.php',
'NuanceItemController' => 'applications/nuance/controller/NuanceItemController.php',
'NuanceItemEditor' => 'applications/nuance/editor/NuanceItemEditor.php',
'NuanceItemListController' => 'applications/nuance/controller/NuanceItemListController.php',
'NuanceItemManageController' => 'applications/nuance/controller/NuanceItemManageController.php',
'NuanceItemOwnerTransaction' => 'applications/nuance/xaction/NuanceItemOwnerTransaction.php',
'NuanceItemPHIDType' => 'applications/nuance/phid/NuanceItemPHIDType.php',
'NuanceItemPropertyTransaction' => 'applications/nuance/xaction/NuanceItemPropertyTransaction.php',
'NuanceItemQuery' => 'applications/nuance/query/NuanceItemQuery.php',
'NuanceItemQueueTransaction' => 'applications/nuance/xaction/NuanceItemQueueTransaction.php',
'NuanceItemRequestorTransaction' => 'applications/nuance/xaction/NuanceItemRequestorTransaction.php',
'NuanceItemSearchEngine' => 'applications/nuance/query/NuanceItemSearchEngine.php',
'NuanceItemSourceTransaction' => 'applications/nuance/xaction/NuanceItemSourceTransaction.php',
'NuanceItemStatusTransaction' => 'applications/nuance/xaction/NuanceItemStatusTransaction.php',
'NuanceItemTransaction' => 'applications/nuance/storage/NuanceItemTransaction.php',
'NuanceItemTransactionComment' => 'applications/nuance/storage/NuanceItemTransactionComment.php',
'NuanceItemTransactionQuery' => 'applications/nuance/query/NuanceItemTransactionQuery.php',
'NuanceItemTransactionType' => 'applications/nuance/xaction/NuanceItemTransactionType.php',
'NuanceItemType' => 'applications/nuance/item/NuanceItemType.php',
'NuanceItemUpdateWorker' => 'applications/nuance/worker/NuanceItemUpdateWorker.php',
'NuanceItemViewController' => 'applications/nuance/controller/NuanceItemViewController.php',
'NuanceManagementImportWorkflow' => 'applications/nuance/management/NuanceManagementImportWorkflow.php',
'NuanceManagementUpdateWorkflow' => 'applications/nuance/management/NuanceManagementUpdateWorkflow.php',
'NuanceManagementWorkflow' => 'applications/nuance/management/NuanceManagementWorkflow.php',
'NuancePhabricatorFormSourceDefinition' => 'applications/nuance/source/NuancePhabricatorFormSourceDefinition.php',
'NuanceQuery' => 'applications/nuance/query/NuanceQuery.php',
'NuanceQueue' => 'applications/nuance/storage/NuanceQueue.php',
'NuanceQueueController' => 'applications/nuance/controller/NuanceQueueController.php',
'NuanceQueueDatasource' => 'applications/nuance/typeahead/NuanceQueueDatasource.php',
'NuanceQueueEditController' => 'applications/nuance/controller/NuanceQueueEditController.php',
'NuanceQueueEditEngine' => 'applications/nuance/editor/NuanceQueueEditEngine.php',
'NuanceQueueEditor' => 'applications/nuance/editor/NuanceQueueEditor.php',
'NuanceQueueListController' => 'applications/nuance/controller/NuanceQueueListController.php',
'NuanceQueueNameTransaction' => 'applications/nuance/xaction/NuanceQueueNameTransaction.php',
'NuanceQueuePHIDType' => 'applications/nuance/phid/NuanceQueuePHIDType.php',
'NuanceQueueQuery' => 'applications/nuance/query/NuanceQueueQuery.php',
'NuanceQueueSearchEngine' => 'applications/nuance/query/NuanceQueueSearchEngine.php',
'NuanceQueueTransaction' => 'applications/nuance/storage/NuanceQueueTransaction.php',
'NuanceQueueTransactionComment' => 'applications/nuance/storage/NuanceQueueTransactionComment.php',
'NuanceQueueTransactionQuery' => 'applications/nuance/query/NuanceQueueTransactionQuery.php',
'NuanceQueueTransactionType' => 'applications/nuance/xaction/NuanceQueueTransactionType.php',
'NuanceQueueViewController' => 'applications/nuance/controller/NuanceQueueViewController.php',
'NuanceQueueWorkController' => 'applications/nuance/controller/NuanceQueueWorkController.php',
'NuanceSchemaSpec' => 'applications/nuance/storage/NuanceSchemaSpec.php',
'NuanceSource' => 'applications/nuance/storage/NuanceSource.php',
'NuanceSourceActionController' => 'applications/nuance/controller/NuanceSourceActionController.php',
'NuanceSourceController' => 'applications/nuance/controller/NuanceSourceController.php',
'NuanceSourceDefaultEditCapability' => 'applications/nuance/capability/NuanceSourceDefaultEditCapability.php',
'NuanceSourceDefaultQueueTransaction' => 'applications/nuance/xaction/NuanceSourceDefaultQueueTransaction.php',
'NuanceSourceDefaultViewCapability' => 'applications/nuance/capability/NuanceSourceDefaultViewCapability.php',
'NuanceSourceDefinition' => 'applications/nuance/source/NuanceSourceDefinition.php',
'NuanceSourceDefinitionTestCase' => 'applications/nuance/source/__tests__/NuanceSourceDefinitionTestCase.php',
'NuanceSourceEditController' => 'applications/nuance/controller/NuanceSourceEditController.php',
'NuanceSourceEditEngine' => 'applications/nuance/editor/NuanceSourceEditEngine.php',
'NuanceSourceEditor' => 'applications/nuance/editor/NuanceSourceEditor.php',
'NuanceSourceListController' => 'applications/nuance/controller/NuanceSourceListController.php',
'NuanceSourceManageCapability' => 'applications/nuance/capability/NuanceSourceManageCapability.php',
'NuanceSourceNameNgrams' => 'applications/nuance/storage/NuanceSourceNameNgrams.php',
'NuanceSourceNameTransaction' => 'applications/nuance/xaction/NuanceSourceNameTransaction.php',
'NuanceSourcePHIDType' => 'applications/nuance/phid/NuanceSourcePHIDType.php',
'NuanceSourceQuery' => 'applications/nuance/query/NuanceSourceQuery.php',
'NuanceSourceSearchEngine' => 'applications/nuance/query/NuanceSourceSearchEngine.php',
'NuanceSourceTransaction' => 'applications/nuance/storage/NuanceSourceTransaction.php',
'NuanceSourceTransactionComment' => 'applications/nuance/storage/NuanceSourceTransactionComment.php',
'NuanceSourceTransactionQuery' => 'applications/nuance/query/NuanceSourceTransactionQuery.php',
'NuanceSourceTransactionType' => 'applications/nuance/xaction/NuanceSourceTransactionType.php',
'NuanceSourceViewController' => 'applications/nuance/controller/NuanceSourceViewController.php',
'NuanceTransaction' => 'applications/nuance/storage/NuanceTransaction.php',
'NuanceTrashCommand' => 'applications/nuance/command/NuanceTrashCommand.php',
'NuanceWorker' => 'applications/nuance/worker/NuanceWorker.php',
'OwnersConduitAPIMethod' => 'applications/owners/conduit/OwnersConduitAPIMethod.php',
'OwnersEditConduitAPIMethod' => 'applications/owners/conduit/OwnersEditConduitAPIMethod.php',
'OwnersPackageReplyHandler' => 'applications/owners/mail/OwnersPackageReplyHandler.php',
'OwnersQueryConduitAPIMethod' => 'applications/owners/conduit/OwnersQueryConduitAPIMethod.php',
'OwnersSearchConduitAPIMethod' => 'applications/owners/conduit/OwnersSearchConduitAPIMethod.php',
'PHIDConduitAPIMethod' => 'applications/phid/conduit/PHIDConduitAPIMethod.php',
'PHIDInfoConduitAPIMethod' => 'applications/phid/conduit/PHIDInfoConduitAPIMethod.php',
'PHIDLookupConduitAPIMethod' => 'applications/phid/conduit/PHIDLookupConduitAPIMethod.php',
'PHIDQueryConduitAPIMethod' => 'applications/phid/conduit/PHIDQueryConduitAPIMethod.php',
'PHUI' => 'view/phui/PHUI.php',
'PHUIActionPanelExample' => 'applications/uiexample/examples/PHUIActionPanelExample.php',
'PHUIActionPanelView' => 'view/phui/PHUIActionPanelView.php',
'PHUIApplicationMenuView' => 'view/layout/PHUIApplicationMenuView.php',
'PHUIBadgeBoxView' => 'view/phui/PHUIBadgeBoxView.php',
'PHUIBadgeExample' => 'applications/uiexample/examples/PHUIBadgeExample.php',
'PHUIBadgeMiniView' => 'view/phui/PHUIBadgeMiniView.php',
'PHUIBadgeView' => 'view/phui/PHUIBadgeView.php',
'PHUIBigInfoExample' => 'applications/uiexample/examples/PHUIBigInfoExample.php',
'PHUIBigInfoView' => 'view/phui/PHUIBigInfoView.php',
'PHUIBoxExample' => 'applications/uiexample/examples/PHUIBoxExample.php',
'PHUIBoxView' => 'view/phui/PHUIBoxView.php',
'PHUIButtonBarExample' => 'applications/uiexample/examples/PHUIButtonBarExample.php',
'PHUIButtonBarView' => 'view/phui/PHUIButtonBarView.php',
'PHUIButtonExample' => 'applications/uiexample/examples/PHUIButtonExample.php',
'PHUIButtonView' => 'view/phui/PHUIButtonView.php',
'PHUICMSView' => 'view/phui/PHUICMSView.php',
'PHUICalendarDayView' => 'view/phui/calendar/PHUICalendarDayView.php',
'PHUICalendarListView' => 'view/phui/calendar/PHUICalendarListView.php',
'PHUICalendarMonthView' => 'view/phui/calendar/PHUICalendarMonthView.php',
'PHUICalendarWeekView' => 'view/phui/calendar/PHUICalendarWeekView.php',
'PHUICalendarWidgetView' => 'view/phui/calendar/PHUICalendarWidgetView.php',
'PHUIColorPalletteExample' => 'applications/uiexample/examples/PHUIColorPalletteExample.php',
'PHUICrumbView' => 'view/phui/PHUICrumbView.php',
'PHUICrumbsView' => 'view/phui/PHUICrumbsView.php',
'PHUICurtainExtension' => 'view/extension/PHUICurtainExtension.php',
'PHUICurtainPanelView' => 'view/layout/PHUICurtainPanelView.php',
'PHUICurtainView' => 'view/layout/PHUICurtainView.php',
'PHUIDiffGraphView' => 'infrastructure/diff/view/PHUIDiffGraphView.php',
'PHUIDiffGraphViewTestCase' => 'infrastructure/diff/view/__tests__/PHUIDiffGraphViewTestCase.php',
'PHUIDiffInlineCommentDetailView' => 'infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php',
'PHUIDiffInlineCommentEditView' => 'infrastructure/diff/view/PHUIDiffInlineCommentEditView.php',
'PHUIDiffInlineCommentPreviewListView' => 'infrastructure/diff/view/PHUIDiffInlineCommentPreviewListView.php',
'PHUIDiffInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffInlineCommentRowScaffold.php',
'PHUIDiffInlineCommentTableScaffold' => 'infrastructure/diff/view/PHUIDiffInlineCommentTableScaffold.php',
'PHUIDiffInlineCommentUndoView' => 'infrastructure/diff/view/PHUIDiffInlineCommentUndoView.php',
'PHUIDiffInlineCommentView' => 'infrastructure/diff/view/PHUIDiffInlineCommentView.php',
'PHUIDiffInlineThreader' => 'infrastructure/diff/view/PHUIDiffInlineThreader.php',
'PHUIDiffOneUpInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffOneUpInlineCommentRowScaffold.php',
'PHUIDiffRevealIconView' => 'infrastructure/diff/view/PHUIDiffRevealIconView.php',
'PHUIDiffTableOfContentsItemView' => 'infrastructure/diff/view/PHUIDiffTableOfContentsItemView.php',
'PHUIDiffTableOfContentsListView' => 'infrastructure/diff/view/PHUIDiffTableOfContentsListView.php',
'PHUIDiffTwoUpInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffTwoUpInlineCommentRowScaffold.php',
'PHUIDocumentSummaryView' => 'view/phui/PHUIDocumentSummaryView.php',
'PHUIDocumentView' => 'view/phui/PHUIDocumentView.php',
'PHUIFeedStoryExample' => 'applications/uiexample/examples/PHUIFeedStoryExample.php',
'PHUIFeedStoryView' => 'view/phui/PHUIFeedStoryView.php',
'PHUIFormDividerControl' => 'view/form/control/PHUIFormDividerControl.php',
'PHUIFormFileControl' => 'view/form/control/PHUIFormFileControl.php',
'PHUIFormFreeformDateControl' => 'view/form/control/PHUIFormFreeformDateControl.php',
'PHUIFormIconSetControl' => 'view/form/control/PHUIFormIconSetControl.php',
'PHUIFormInsetView' => 'view/form/PHUIFormInsetView.php',
'PHUIFormLayoutView' => 'view/form/PHUIFormLayoutView.php',
'PHUIFormNumberControl' => 'view/form/control/PHUIFormNumberControl.php',
'PHUIFormTimerControl' => 'view/form/control/PHUIFormTimerControl.php',
'PHUIHandleListView' => 'applications/phid/view/PHUIHandleListView.php',
'PHUIHandleTagListView' => 'applications/phid/view/PHUIHandleTagListView.php',
'PHUIHandleView' => 'applications/phid/view/PHUIHandleView.php',
'PHUIHeadThingView' => 'view/phui/PHUIHeadThingView.php',
'PHUIHeaderView' => 'view/phui/PHUIHeaderView.php',
'PHUIHomeView' => 'applications/home/view/PHUIHomeView.php',
'PHUIHovercardUIExample' => 'applications/uiexample/examples/PHUIHovercardUIExample.php',
'PHUIHovercardView' => 'view/phui/PHUIHovercardView.php',
'PHUIIconCircleView' => 'view/phui/PHUIIconCircleView.php',
'PHUIIconExample' => 'applications/uiexample/examples/PHUIIconExample.php',
'PHUIIconView' => 'view/phui/PHUIIconView.php',
'PHUIImageMaskExample' => 'applications/uiexample/examples/PHUIImageMaskExample.php',
'PHUIImageMaskView' => 'view/phui/PHUIImageMaskView.php',
'PHUIInfoExample' => 'applications/uiexample/examples/PHUIInfoExample.php',
'PHUIInfoView' => 'view/phui/PHUIInfoView.php',
'PHUIInvisibleCharacterTestCase' => 'view/phui/__tests__/PHUIInvisibleCharacterTestCase.php',
'PHUIInvisibleCharacterView' => 'view/phui/PHUIInvisibleCharacterView.php',
'PHUILeftRightExample' => 'applications/uiexample/examples/PHUILeftRightExample.php',
'PHUILeftRightView' => 'view/phui/PHUILeftRightView.php',
'PHUIListExample' => 'applications/uiexample/examples/PHUIListExample.php',
'PHUIListItemView' => 'view/phui/PHUIListItemView.php',
'PHUIListView' => 'view/phui/PHUIListView.php',
'PHUIListViewTestCase' => 'view/layout/__tests__/PHUIListViewTestCase.php',
'PHUIObjectBoxView' => 'view/phui/PHUIObjectBoxView.php',
'PHUIObjectItemListExample' => 'applications/uiexample/examples/PHUIObjectItemListExample.php',
'PHUIObjectItemListView' => 'view/phui/PHUIObjectItemListView.php',
'PHUIObjectItemView' => 'view/phui/PHUIObjectItemView.php',
'PHUIPagerView' => 'view/phui/PHUIPagerView.php',
'PHUIPinboardItemView' => 'view/phui/PHUIPinboardItemView.php',
'PHUIPinboardView' => 'view/phui/PHUIPinboardView.php',
'PHUIPolicySectionView' => 'applications/policy/view/PHUIPolicySectionView.php',
'PHUIPropertyGroupView' => 'view/phui/PHUIPropertyGroupView.php',
'PHUIPropertyListExample' => 'applications/uiexample/examples/PHUIPropertyListExample.php',
'PHUIPropertyListView' => 'view/phui/PHUIPropertyListView.php',
'PHUIRemarkupImageView' => 'infrastructure/markup/view/PHUIRemarkupImageView.php',
'PHUIRemarkupPreviewPanel' => 'view/phui/PHUIRemarkupPreviewPanel.php',
'PHUIRemarkupView' => 'infrastructure/markup/view/PHUIRemarkupView.php',
'PHUISegmentBarSegmentView' => 'view/phui/PHUISegmentBarSegmentView.php',
'PHUISegmentBarView' => 'view/phui/PHUISegmentBarView.php',
'PHUISpacesNamespaceContextView' => 'applications/spaces/view/PHUISpacesNamespaceContextView.php',
'PHUIStatusItemView' => 'view/phui/PHUIStatusItemView.php',
'PHUIStatusListView' => 'view/phui/PHUIStatusListView.php',
'PHUITabGroupView' => 'view/phui/PHUITabGroupView.php',
'PHUITabView' => 'view/phui/PHUITabView.php',
'PHUITagExample' => 'applications/uiexample/examples/PHUITagExample.php',
'PHUITagView' => 'view/phui/PHUITagView.php',
'PHUITimelineEventView' => 'view/phui/PHUITimelineEventView.php',
'PHUITimelineExample' => 'applications/uiexample/examples/PHUITimelineExample.php',
'PHUITimelineView' => 'view/phui/PHUITimelineView.php',
'PHUITwoColumnView' => 'view/phui/PHUITwoColumnView.php',
'PHUITypeaheadExample' => 'applications/uiexample/examples/PHUITypeaheadExample.php',
'PHUIUserAvailabilityView' => 'applications/calendar/view/PHUIUserAvailabilityView.php',
'PHUIWorkboardView' => 'view/phui/PHUIWorkboardView.php',
'PHUIWorkpanelView' => 'view/phui/PHUIWorkpanelView.php',
'PHUIXComponentsExample' => 'applications/uiexample/examples/PHUIXComponentsExample.php',
'PassphraseAbstractKey' => 'applications/passphrase/keys/PassphraseAbstractKey.php',
'PassphraseConduitAPIMethod' => 'applications/passphrase/conduit/PassphraseConduitAPIMethod.php',
'PassphraseController' => 'applications/passphrase/controller/PassphraseController.php',
'PassphraseCredential' => 'applications/passphrase/storage/PassphraseCredential.php',
'PassphraseCredentialAuthorPolicyRule' => 'applications/passphrase/policyrule/PassphraseCredentialAuthorPolicyRule.php',
'PassphraseCredentialConduitController' => 'applications/passphrase/controller/PassphraseCredentialConduitController.php',
'PassphraseCredentialConduitTransaction' => 'applications/passphrase/xaction/PassphraseCredentialConduitTransaction.php',
'PassphraseCredentialControl' => 'applications/passphrase/view/PassphraseCredentialControl.php',
'PassphraseCredentialCreateController' => 'applications/passphrase/controller/PassphraseCredentialCreateController.php',
'PassphraseCredentialDescriptionTransaction' => 'applications/passphrase/xaction/PassphraseCredentialDescriptionTransaction.php',
'PassphraseCredentialDestroyController' => 'applications/passphrase/controller/PassphraseCredentialDestroyController.php',
'PassphraseCredentialDestroyTransaction' => 'applications/passphrase/xaction/PassphraseCredentialDestroyTransaction.php',
'PassphraseCredentialEditController' => 'applications/passphrase/controller/PassphraseCredentialEditController.php',
'PassphraseCredentialFerretEngine' => 'applications/passphrase/search/PassphraseCredentialFerretEngine.php',
'PassphraseCredentialFulltextEngine' => 'applications/passphrase/search/PassphraseCredentialFulltextEngine.php',
'PassphraseCredentialListController' => 'applications/passphrase/controller/PassphraseCredentialListController.php',
'PassphraseCredentialLockController' => 'applications/passphrase/controller/PassphraseCredentialLockController.php',
'PassphraseCredentialLockTransaction' => 'applications/passphrase/xaction/PassphraseCredentialLockTransaction.php',
'PassphraseCredentialLookedAtTransaction' => 'applications/passphrase/xaction/PassphraseCredentialLookedAtTransaction.php',
'PassphraseCredentialNameTransaction' => 'applications/passphrase/xaction/PassphraseCredentialNameTransaction.php',
'PassphraseCredentialPHIDType' => 'applications/passphrase/phid/PassphraseCredentialPHIDType.php',
'PassphraseCredentialPublicController' => 'applications/passphrase/controller/PassphraseCredentialPublicController.php',
'PassphraseCredentialQuery' => 'applications/passphrase/query/PassphraseCredentialQuery.php',
'PassphraseCredentialRevealController' => 'applications/passphrase/controller/PassphraseCredentialRevealController.php',
'PassphraseCredentialSearchEngine' => 'applications/passphrase/query/PassphraseCredentialSearchEngine.php',
'PassphraseCredentialSecretIDTransaction' => 'applications/passphrase/xaction/PassphraseCredentialSecretIDTransaction.php',
'PassphraseCredentialTransaction' => 'applications/passphrase/storage/PassphraseCredentialTransaction.php',
'PassphraseCredentialTransactionEditor' => 'applications/passphrase/editor/PassphraseCredentialTransactionEditor.php',
'PassphraseCredentialTransactionQuery' => 'applications/passphrase/query/PassphraseCredentialTransactionQuery.php',
'PassphraseCredentialTransactionType' => 'applications/passphrase/xaction/PassphraseCredentialTransactionType.php',
'PassphraseCredentialType' => 'applications/passphrase/credentialtype/PassphraseCredentialType.php',
'PassphraseCredentialTypeTestCase' => 'applications/passphrase/credentialtype/__tests__/PassphraseCredentialTypeTestCase.php',
'PassphraseCredentialUsernameTransaction' => 'applications/passphrase/xaction/PassphraseCredentialUsernameTransaction.php',
'PassphraseCredentialViewController' => 'applications/passphrase/controller/PassphraseCredentialViewController.php',
'PassphraseDAO' => 'applications/passphrase/storage/PassphraseDAO.php',
'PassphraseDefaultEditCapability' => 'applications/passphrase/capability/PassphraseDefaultEditCapability.php',
'PassphraseDefaultViewCapability' => 'applications/passphrase/capability/PassphraseDefaultViewCapability.php',
'PassphraseNoteCredentialType' => 'applications/passphrase/credentialtype/PassphraseNoteCredentialType.php',
'PassphrasePasswordCredentialType' => 'applications/passphrase/credentialtype/PassphrasePasswordCredentialType.php',
'PassphrasePasswordKey' => 'applications/passphrase/keys/PassphrasePasswordKey.php',
'PassphraseQueryConduitAPIMethod' => 'applications/passphrase/conduit/PassphraseQueryConduitAPIMethod.php',
'PassphraseRemarkupRule' => 'applications/passphrase/remarkup/PassphraseRemarkupRule.php',
'PassphraseSSHGeneratedKeyCredentialType' => 'applications/passphrase/credentialtype/PassphraseSSHGeneratedKeyCredentialType.php',
'PassphraseSSHKey' => 'applications/passphrase/keys/PassphraseSSHKey.php',
'PassphraseSSHPrivateKeyCredentialType' => 'applications/passphrase/credentialtype/PassphraseSSHPrivateKeyCredentialType.php',
'PassphraseSSHPrivateKeyFileCredentialType' => 'applications/passphrase/credentialtype/PassphraseSSHPrivateKeyFileCredentialType.php',
'PassphraseSSHPrivateKeyTextCredentialType' => 'applications/passphrase/credentialtype/PassphraseSSHPrivateKeyTextCredentialType.php',
'PassphraseSchemaSpec' => 'applications/passphrase/storage/PassphraseSchemaSpec.php',
'PassphraseSecret' => 'applications/passphrase/storage/PassphraseSecret.php',
'PassphraseTokenCredentialType' => 'applications/passphrase/credentialtype/PassphraseTokenCredentialType.php',
'PasteConduitAPIMethod' => 'applications/paste/conduit/PasteConduitAPIMethod.php',
'PasteCreateConduitAPIMethod' => 'applications/paste/conduit/PasteCreateConduitAPIMethod.php',
'PasteCreateMailReceiver' => 'applications/paste/mail/PasteCreateMailReceiver.php',
'PasteDefaultEditCapability' => 'applications/paste/capability/PasteDefaultEditCapability.php',
'PasteDefaultViewCapability' => 'applications/paste/capability/PasteDefaultViewCapability.php',
'PasteEditConduitAPIMethod' => 'applications/paste/conduit/PasteEditConduitAPIMethod.php',
'PasteEmbedView' => 'applications/paste/view/PasteEmbedView.php',
'PasteInfoConduitAPIMethod' => 'applications/paste/conduit/PasteInfoConduitAPIMethod.php',
'PasteLanguageSelectDatasource' => 'applications/paste/typeahead/PasteLanguageSelectDatasource.php',
'PasteMailReceiver' => 'applications/paste/mail/PasteMailReceiver.php',
'PasteQueryConduitAPIMethod' => 'applications/paste/conduit/PasteQueryConduitAPIMethod.php',
'PasteReplyHandler' => 'applications/paste/mail/PasteReplyHandler.php',
'PasteSearchConduitAPIMethod' => 'applications/paste/conduit/PasteSearchConduitAPIMethod.php',
'PeopleBrowseUserDirectoryCapability' => 'applications/people/capability/PeopleBrowseUserDirectoryCapability.php',
'PeopleCreateUsersCapability' => 'applications/people/capability/PeopleCreateUsersCapability.php',
'PeopleDisableUsersCapability' => 'applications/people/capability/PeopleDisableUsersCapability.php',
'PeopleHovercardEngineExtension' => 'applications/people/engineextension/PeopleHovercardEngineExtension.php',
'PeopleMainMenuBarExtension' => 'applications/people/engineextension/PeopleMainMenuBarExtension.php',
'PeopleUserLogGarbageCollector' => 'applications/people/garbagecollector/PeopleUserLogGarbageCollector.php',
'Phabricator404Controller' => 'applications/base/controller/Phabricator404Controller.php',
'PhabricatorAWSConfigOptions' => 'applications/config/option/PhabricatorAWSConfigOptions.php',
'PhabricatorAccessControlTestCase' => 'applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php',
'PhabricatorAccessLog' => 'infrastructure/log/PhabricatorAccessLog.php',
'PhabricatorAccessLogConfigOptions' => 'applications/config/option/PhabricatorAccessLogConfigOptions.php',
'PhabricatorAccessibilitySetting' => 'applications/settings/setting/PhabricatorAccessibilitySetting.php',
'PhabricatorActionListView' => 'view/layout/PhabricatorActionListView.php',
'PhabricatorActionView' => 'view/layout/PhabricatorActionView.php',
'PhabricatorActivitySettingsPanel' => 'applications/settings/panel/PhabricatorActivitySettingsPanel.php',
'PhabricatorAdministratorsPolicyRule' => 'applications/people/policyrule/PhabricatorAdministratorsPolicyRule.php',
'PhabricatorAjaxRequestExceptionHandler' => 'aphront/handler/PhabricatorAjaxRequestExceptionHandler.php',
'PhabricatorAlmanacApplication' => 'applications/almanac/application/PhabricatorAlmanacApplication.php',
'PhabricatorAmazonAuthProvider' => 'applications/auth/provider/PhabricatorAmazonAuthProvider.php',
'PhabricatorAmazonSNSFuture' => 'applications/metamta/future/PhabricatorAmazonSNSFuture.php',
'PhabricatorAnchorView' => 'view/layout/PhabricatorAnchorView.php',
'PhabricatorAphlictManagementDebugWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementDebugWorkflow.php',
'PhabricatorAphlictManagementNotifyWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementNotifyWorkflow.php',
'PhabricatorAphlictManagementRestartWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementRestartWorkflow.php',
'PhabricatorAphlictManagementStartWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementStartWorkflow.php',
'PhabricatorAphlictManagementStatusWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementStatusWorkflow.php',
'PhabricatorAphlictManagementStopWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementStopWorkflow.php',
'PhabricatorAphlictManagementWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php',
'PhabricatorAphlictSetupCheck' => 'applications/notification/setup/PhabricatorAphlictSetupCheck.php',
'PhabricatorAphrontBarUIExample' => 'applications/uiexample/examples/PhabricatorAphrontBarUIExample.php',
'PhabricatorAphrontViewTestCase' => 'view/__tests__/PhabricatorAphrontViewTestCase.php',
'PhabricatorAppSearchEngine' => 'applications/meta/query/PhabricatorAppSearchEngine.php',
'PhabricatorApplication' => 'applications/base/PhabricatorApplication.php',
'PhabricatorApplicationApplicationPHIDType' => 'applications/meta/phid/PhabricatorApplicationApplicationPHIDType.php',
'PhabricatorApplicationApplicationTransaction' => 'applications/meta/storage/PhabricatorApplicationApplicationTransaction.php',
'PhabricatorApplicationApplicationTransactionQuery' => 'applications/meta/query/PhabricatorApplicationApplicationTransactionQuery.php',
'PhabricatorApplicationConfigOptions' => 'applications/config/option/PhabricatorApplicationConfigOptions.php',
'PhabricatorApplicationConfigurationPanel' => 'applications/meta/panel/PhabricatorApplicationConfigurationPanel.php',
'PhabricatorApplicationConfigurationPanelTestCase' => 'applications/meta/panel/__tests__/PhabricatorApplicationConfigurationPanelTestCase.php',
'PhabricatorApplicationDatasource' => 'applications/meta/typeahead/PhabricatorApplicationDatasource.php',
'PhabricatorApplicationDetailViewController' => 'applications/meta/controller/PhabricatorApplicationDetailViewController.php',
'PhabricatorApplicationEditController' => 'applications/meta/controller/PhabricatorApplicationEditController.php',
'PhabricatorApplicationEditEngine' => 'applications/meta/editor/PhabricatorApplicationEditEngine.php',
'PhabricatorApplicationEditHTTPParameterHelpView' => 'applications/transactions/view/PhabricatorApplicationEditHTTPParameterHelpView.php',
'PhabricatorApplicationEditor' => 'applications/meta/editor/PhabricatorApplicationEditor.php',
'PhabricatorApplicationEmailCommandsController' => 'applications/meta/controller/PhabricatorApplicationEmailCommandsController.php',
'PhabricatorApplicationMailReceiver' => 'applications/metamta/receiver/PhabricatorApplicationMailReceiver.php',
'PhabricatorApplicationObjectMailEngineExtension' => 'applications/transactions/engineextension/PhabricatorApplicationObjectMailEngineExtension.php',
'PhabricatorApplicationPanelController' => 'applications/meta/controller/PhabricatorApplicationPanelController.php',
'PhabricatorApplicationPolicyChangeTransaction' => 'applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php',
'PhabricatorApplicationProfileMenuItem' => 'applications/search/menuitem/PhabricatorApplicationProfileMenuItem.php',
'PhabricatorApplicationQuery' => 'applications/meta/query/PhabricatorApplicationQuery.php',
'PhabricatorApplicationSchemaSpec' => 'applications/meta/storage/PhabricatorApplicationSchemaSpec.php',
'PhabricatorApplicationSearchController' => 'applications/search/controller/PhabricatorApplicationSearchController.php',
'PhabricatorApplicationSearchEngine' => 'applications/search/engine/PhabricatorApplicationSearchEngine.php',
'PhabricatorApplicationSearchEngineTestCase' => 'applications/search/engine/__tests__/PhabricatorApplicationSearchEngineTestCase.php',
'PhabricatorApplicationSearchResultView' => 'applications/search/view/PhabricatorApplicationSearchResultView.php',
'PhabricatorApplicationTestCase' => 'applications/base/__tests__/PhabricatorApplicationTestCase.php',
'PhabricatorApplicationTransaction' => 'applications/transactions/storage/PhabricatorApplicationTransaction.php',
'PhabricatorApplicationTransactionComment' => 'applications/transactions/storage/PhabricatorApplicationTransactionComment.php',
'PhabricatorApplicationTransactionCommentEditController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php',
'PhabricatorApplicationTransactionCommentEditor' => 'applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php',
'PhabricatorApplicationTransactionCommentHistoryController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentHistoryController.php',
'PhabricatorApplicationTransactionCommentQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionCommentQuery.php',
'PhabricatorApplicationTransactionCommentQuoteController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentQuoteController.php',
'PhabricatorApplicationTransactionCommentRawController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentRawController.php',
'PhabricatorApplicationTransactionCommentRemoveController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php',
'PhabricatorApplicationTransactionCommentView' => 'applications/transactions/view/PhabricatorApplicationTransactionCommentView.php',
'PhabricatorApplicationTransactionController' => 'applications/transactions/controller/PhabricatorApplicationTransactionController.php',
'PhabricatorApplicationTransactionDetailController' => 'applications/transactions/controller/PhabricatorApplicationTransactionDetailController.php',
'PhabricatorApplicationTransactionEditor' => 'applications/transactions/editor/PhabricatorApplicationTransactionEditor.php',
'PhabricatorApplicationTransactionFeedStory' => 'applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php',
'PhabricatorApplicationTransactionInterface' => 'applications/transactions/interface/PhabricatorApplicationTransactionInterface.php',
'PhabricatorApplicationTransactionNoEffectException' => 'applications/transactions/exception/PhabricatorApplicationTransactionNoEffectException.php',
'PhabricatorApplicationTransactionNoEffectResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionNoEffectResponse.php',
'PhabricatorApplicationTransactionPublishWorker' => 'applications/transactions/worker/PhabricatorApplicationTransactionPublishWorker.php',
'PhabricatorApplicationTransactionQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionQuery.php',
'PhabricatorApplicationTransactionRemarkupPreviewController' => 'applications/transactions/controller/PhabricatorApplicationTransactionRemarkupPreviewController.php',
'PhabricatorApplicationTransactionReplyHandler' => 'applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php',
'PhabricatorApplicationTransactionResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionResponse.php',
'PhabricatorApplicationTransactionShowOlderController' => 'applications/transactions/controller/PhabricatorApplicationTransactionShowOlderController.php',
'PhabricatorApplicationTransactionStructureException' => 'applications/transactions/exception/PhabricatorApplicationTransactionStructureException.php',
'PhabricatorApplicationTransactionTemplatedCommentQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionTemplatedCommentQuery.php',
'PhabricatorApplicationTransactionTextDiffDetailView' => 'applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php',
'PhabricatorApplicationTransactionTransactionPHIDType' => 'applications/transactions/phid/PhabricatorApplicationTransactionTransactionPHIDType.php',
'PhabricatorApplicationTransactionType' => 'applications/meta/xactions/PhabricatorApplicationTransactionType.php',
'PhabricatorApplicationTransactionValidationError' => 'applications/transactions/error/PhabricatorApplicationTransactionValidationError.php',
'PhabricatorApplicationTransactionValidationException' => 'applications/transactions/exception/PhabricatorApplicationTransactionValidationException.php',
'PhabricatorApplicationTransactionValidationResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionValidationResponse.php',
'PhabricatorApplicationTransactionValueController' => 'applications/transactions/controller/PhabricatorApplicationTransactionValueController.php',
'PhabricatorApplicationTransactionView' => 'applications/transactions/view/PhabricatorApplicationTransactionView.php',
'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',
'PhabricatorApplyEditField' => 'applications/transactions/editfield/PhabricatorApplyEditField.php',
'PhabricatorAsanaAuthProvider' => 'applications/auth/provider/PhabricatorAsanaAuthProvider.php',
'PhabricatorAsanaConfigOptions' => 'applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php',
'PhabricatorAsanaSubtaskHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorAsanaSubtaskHasObjectEdgeType.php',
'PhabricatorAsanaTaskHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorAsanaTaskHasObjectEdgeType.php',
'PhabricatorAudioDocumentEngine' => 'applications/files/document/PhabricatorAudioDocumentEngine.php',
'PhabricatorAuditActionConstants' => 'applications/audit/constants/PhabricatorAuditActionConstants.php',
'PhabricatorAuditApplication' => 'applications/audit/application/PhabricatorAuditApplication.php',
'PhabricatorAuditCommentEditor' => 'applications/audit/editor/PhabricatorAuditCommentEditor.php',
'PhabricatorAuditController' => 'applications/audit/controller/PhabricatorAuditController.php',
'PhabricatorAuditEditor' => 'applications/audit/editor/PhabricatorAuditEditor.php',
'PhabricatorAuditInlineComment' => 'applications/audit/storage/PhabricatorAuditInlineComment.php',
'PhabricatorAuditListView' => 'applications/audit/view/PhabricatorAuditListView.php',
'PhabricatorAuditMailReceiver' => 'applications/audit/mail/PhabricatorAuditMailReceiver.php',
'PhabricatorAuditManagementDeleteWorkflow' => 'applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php',
'PhabricatorAuditManagementWorkflow' => 'applications/audit/management/PhabricatorAuditManagementWorkflow.php',
'PhabricatorAuditReplyHandler' => 'applications/audit/mail/PhabricatorAuditReplyHandler.php',
'PhabricatorAuditStatusConstants' => 'applications/audit/constants/PhabricatorAuditStatusConstants.php',
'PhabricatorAuditSynchronizeManagementWorkflow' => 'applications/audit/management/PhabricatorAuditSynchronizeManagementWorkflow.php',
'PhabricatorAuditTransaction' => 'applications/audit/storage/PhabricatorAuditTransaction.php',
'PhabricatorAuditTransactionComment' => 'applications/audit/storage/PhabricatorAuditTransactionComment.php',
'PhabricatorAuditTransactionQuery' => 'applications/audit/query/PhabricatorAuditTransactionQuery.php',
'PhabricatorAuditTransactionView' => 'applications/audit/view/PhabricatorAuditTransactionView.php',
'PhabricatorAuditUpdateOwnersManagementWorkflow' => 'applications/audit/management/PhabricatorAuditUpdateOwnersManagementWorkflow.php',
'PhabricatorAuthAccountView' => 'applications/auth/view/PhabricatorAuthAccountView.php',
'PhabricatorAuthApplication' => 'applications/auth/application/PhabricatorAuthApplication.php',
'PhabricatorAuthAuthFactorPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthFactorPHIDType.php',
'PhabricatorAuthAuthFactorProviderPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthFactorProviderPHIDType.php',
'PhabricatorAuthAuthProviderPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthProviderPHIDType.php',
'PhabricatorAuthCSRFEngine' => 'applications/auth/engine/PhabricatorAuthCSRFEngine.php',
'PhabricatorAuthChallenge' => 'applications/auth/storage/PhabricatorAuthChallenge.php',
'PhabricatorAuthChallengeGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthChallengeGarbageCollector.php',
'PhabricatorAuthChallengePHIDType' => 'applications/auth/phid/PhabricatorAuthChallengePHIDType.php',
'PhabricatorAuthChallengeQuery' => 'applications/auth/query/PhabricatorAuthChallengeQuery.php',
+ 'PhabricatorAuthChallengeStatusController' => 'applications/auth/controller/mfa/PhabricatorAuthChallengeStatusController.php',
+ 'PhabricatorAuthChallengeUpdate' => 'applications/auth/view/PhabricatorAuthChallengeUpdate.php',
'PhabricatorAuthChangePasswordAction' => 'applications/auth/action/PhabricatorAuthChangePasswordAction.php',
'PhabricatorAuthConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php',
'PhabricatorAuthConduitTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthConduitTokenRevoker.php',
'PhabricatorAuthConfirmLinkController' => 'applications/auth/controller/PhabricatorAuthConfirmLinkController.php',
'PhabricatorAuthContactNumber' => 'applications/auth/storage/PhabricatorAuthContactNumber.php',
'PhabricatorAuthContactNumberController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberController.php',
'PhabricatorAuthContactNumberDisableController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberDisableController.php',
'PhabricatorAuthContactNumberEditController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberEditController.php',
'PhabricatorAuthContactNumberEditEngine' => 'applications/auth/editor/PhabricatorAuthContactNumberEditEngine.php',
'PhabricatorAuthContactNumberEditor' => 'applications/auth/editor/PhabricatorAuthContactNumberEditor.php',
'PhabricatorAuthContactNumberMFAEngine' => 'applications/auth/engine/PhabricatorAuthContactNumberMFAEngine.php',
'PhabricatorAuthContactNumberNumberTransaction' => 'applications/auth/xaction/PhabricatorAuthContactNumberNumberTransaction.php',
'PhabricatorAuthContactNumberPHIDType' => 'applications/auth/phid/PhabricatorAuthContactNumberPHIDType.php',
'PhabricatorAuthContactNumberPrimaryController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php',
'PhabricatorAuthContactNumberPrimaryTransaction' => 'applications/auth/xaction/PhabricatorAuthContactNumberPrimaryTransaction.php',
'PhabricatorAuthContactNumberQuery' => 'applications/auth/query/PhabricatorAuthContactNumberQuery.php',
'PhabricatorAuthContactNumberStatusTransaction' => 'applications/auth/xaction/PhabricatorAuthContactNumberStatusTransaction.php',
'PhabricatorAuthContactNumberTestController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberTestController.php',
'PhabricatorAuthContactNumberTransaction' => 'applications/auth/storage/PhabricatorAuthContactNumberTransaction.php',
'PhabricatorAuthContactNumberTransactionQuery' => 'applications/auth/query/PhabricatorAuthContactNumberTransactionQuery.php',
'PhabricatorAuthContactNumberTransactionType' => 'applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php',
'PhabricatorAuthContactNumberViewController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php',
'PhabricatorAuthController' => 'applications/auth/controller/PhabricatorAuthController.php',
'PhabricatorAuthDAO' => 'applications/auth/storage/PhabricatorAuthDAO.php',
'PhabricatorAuthDisableController' => 'applications/auth/controller/config/PhabricatorAuthDisableController.php',
'PhabricatorAuthDowngradeSessionController' => 'applications/auth/controller/PhabricatorAuthDowngradeSessionController.php',
'PhabricatorAuthEditController' => 'applications/auth/controller/config/PhabricatorAuthEditController.php',
'PhabricatorAuthFactor' => 'applications/auth/factor/PhabricatorAuthFactor.php',
'PhabricatorAuthFactorConfig' => 'applications/auth/storage/PhabricatorAuthFactorConfig.php',
'PhabricatorAuthFactorConfigQuery' => 'applications/auth/query/PhabricatorAuthFactorConfigQuery.php',
'PhabricatorAuthFactorProvider' => 'applications/auth/storage/PhabricatorAuthFactorProvider.php',
'PhabricatorAuthFactorProviderController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderController.php',
'PhabricatorAuthFactorProviderDuoCredentialTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderDuoCredentialTransaction.php',
'PhabricatorAuthFactorProviderDuoEnrollTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderDuoEnrollTransaction.php',
'PhabricatorAuthFactorProviderDuoHostnameTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderDuoHostnameTransaction.php',
'PhabricatorAuthFactorProviderDuoUsernamesTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderDuoUsernamesTransaction.php',
'PhabricatorAuthFactorProviderEditController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php',
'PhabricatorAuthFactorProviderEditEngine' => 'applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php',
'PhabricatorAuthFactorProviderEditor' => 'applications/auth/editor/PhabricatorAuthFactorProviderEditor.php',
'PhabricatorAuthFactorProviderEnrollMessageTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderEnrollMessageTransaction.php',
'PhabricatorAuthFactorProviderListController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.php',
'PhabricatorAuthFactorProviderMFAEngine' => 'applications/auth/engine/PhabricatorAuthFactorProviderMFAEngine.php',
'PhabricatorAuthFactorProviderMessageController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderMessageController.php',
'PhabricatorAuthFactorProviderNameTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderNameTransaction.php',
'PhabricatorAuthFactorProviderQuery' => 'applications/auth/query/PhabricatorAuthFactorProviderQuery.php',
'PhabricatorAuthFactorProviderStatus' => 'applications/auth/constants/PhabricatorAuthFactorProviderStatus.php',
'PhabricatorAuthFactorProviderStatusTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderStatusTransaction.php',
'PhabricatorAuthFactorProviderTransaction' => 'applications/auth/storage/PhabricatorAuthFactorProviderTransaction.php',
'PhabricatorAuthFactorProviderTransactionQuery' => 'applications/auth/query/PhabricatorAuthFactorProviderTransactionQuery.php',
'PhabricatorAuthFactorProviderTransactionType' => 'applications/auth/xaction/PhabricatorAuthFactorProviderTransactionType.php',
'PhabricatorAuthFactorProviderViewController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php',
'PhabricatorAuthFactorResult' => 'applications/auth/factor/PhabricatorAuthFactorResult.php',
'PhabricatorAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthFactorTestCase.php',
'PhabricatorAuthFinishController' => 'applications/auth/controller/PhabricatorAuthFinishController.php',
'PhabricatorAuthHMACKey' => 'applications/auth/storage/PhabricatorAuthHMACKey.php',
'PhabricatorAuthHighSecurityRequiredException' => 'applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php',
'PhabricatorAuthHighSecurityToken' => 'applications/auth/data/PhabricatorAuthHighSecurityToken.php',
'PhabricatorAuthInvite' => 'applications/auth/storage/PhabricatorAuthInvite.php',
'PhabricatorAuthInviteAccountException' => 'applications/auth/exception/PhabricatorAuthInviteAccountException.php',
'PhabricatorAuthInviteAction' => 'applications/auth/data/PhabricatorAuthInviteAction.php',
'PhabricatorAuthInviteActionTableView' => 'applications/auth/view/PhabricatorAuthInviteActionTableView.php',
'PhabricatorAuthInviteController' => 'applications/auth/controller/PhabricatorAuthInviteController.php',
'PhabricatorAuthInviteDialogException' => 'applications/auth/exception/PhabricatorAuthInviteDialogException.php',
'PhabricatorAuthInviteEngine' => 'applications/auth/engine/PhabricatorAuthInviteEngine.php',
'PhabricatorAuthInviteException' => 'applications/auth/exception/PhabricatorAuthInviteException.php',
'PhabricatorAuthInviteInvalidException' => 'applications/auth/exception/PhabricatorAuthInviteInvalidException.php',
'PhabricatorAuthInviteLoginException' => 'applications/auth/exception/PhabricatorAuthInviteLoginException.php',
'PhabricatorAuthInvitePHIDType' => 'applications/auth/phid/PhabricatorAuthInvitePHIDType.php',
'PhabricatorAuthInviteQuery' => 'applications/auth/query/PhabricatorAuthInviteQuery.php',
'PhabricatorAuthInviteRegisteredException' => 'applications/auth/exception/PhabricatorAuthInviteRegisteredException.php',
'PhabricatorAuthInviteSearchEngine' => 'applications/auth/query/PhabricatorAuthInviteSearchEngine.php',
'PhabricatorAuthInviteTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthInviteTestCase.php',
'PhabricatorAuthInviteVerifyException' => 'applications/auth/exception/PhabricatorAuthInviteVerifyException.php',
'PhabricatorAuthInviteWorker' => 'applications/auth/worker/PhabricatorAuthInviteWorker.php',
'PhabricatorAuthLinkController' => 'applications/auth/controller/PhabricatorAuthLinkController.php',
+ 'PhabricatorAuthLinkMessageType' => 'applications/auth/message/PhabricatorAuthLinkMessageType.php',
'PhabricatorAuthListController' => 'applications/auth/controller/config/PhabricatorAuthListController.php',
'PhabricatorAuthLoginController' => 'applications/auth/controller/PhabricatorAuthLoginController.php',
- 'PhabricatorAuthLoginHandler' => 'applications/auth/handler/PhabricatorAuthLoginHandler.php',
'PhabricatorAuthLoginMessageType' => 'applications/auth/message/PhabricatorAuthLoginMessageType.php',
'PhabricatorAuthLogoutConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthLogoutConduitAPIMethod.php',
'PhabricatorAuthMFAEditEngineExtension' => 'applications/auth/engineextension/PhabricatorAuthMFAEditEngineExtension.php',
'PhabricatorAuthMFASyncTemporaryTokenType' => 'applications/auth/factor/PhabricatorAuthMFASyncTemporaryTokenType.php',
'PhabricatorAuthMainMenuBarExtension' => 'applications/auth/extension/PhabricatorAuthMainMenuBarExtension.php',
'PhabricatorAuthManagementCachePKCS8Workflow' => 'applications/auth/management/PhabricatorAuthManagementCachePKCS8Workflow.php',
'PhabricatorAuthManagementLDAPWorkflow' => 'applications/auth/management/PhabricatorAuthManagementLDAPWorkflow.php',
'PhabricatorAuthManagementListFactorsWorkflow' => 'applications/auth/management/PhabricatorAuthManagementListFactorsWorkflow.php',
'PhabricatorAuthManagementListMFAProvidersWorkflow' => 'applications/auth/management/PhabricatorAuthManagementListMFAProvidersWorkflow.php',
'PhabricatorAuthManagementRecoverWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php',
'PhabricatorAuthManagementRefreshWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php',
'PhabricatorAuthManagementRevokeWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRevokeWorkflow.php',
'PhabricatorAuthManagementStripWorkflow' => 'applications/auth/management/PhabricatorAuthManagementStripWorkflow.php',
'PhabricatorAuthManagementTrustOAuthClientWorkflow' => 'applications/auth/management/PhabricatorAuthManagementTrustOAuthClientWorkflow.php',
'PhabricatorAuthManagementUnlimitWorkflow' => 'applications/auth/management/PhabricatorAuthManagementUnlimitWorkflow.php',
'PhabricatorAuthManagementUntrustOAuthClientWorkflow' => 'applications/auth/management/PhabricatorAuthManagementUntrustOAuthClientWorkflow.php',
'PhabricatorAuthManagementVerifyWorkflow' => 'applications/auth/management/PhabricatorAuthManagementVerifyWorkflow.php',
'PhabricatorAuthManagementWorkflow' => 'applications/auth/management/PhabricatorAuthManagementWorkflow.php',
'PhabricatorAuthMessage' => 'applications/auth/storage/PhabricatorAuthMessage.php',
'PhabricatorAuthMessageController' => 'applications/auth/controller/message/PhabricatorAuthMessageController.php',
'PhabricatorAuthMessageEditController' => 'applications/auth/controller/message/PhabricatorAuthMessageEditController.php',
'PhabricatorAuthMessageEditEngine' => 'applications/auth/editor/PhabricatorAuthMessageEditEngine.php',
'PhabricatorAuthMessageEditor' => 'applications/auth/editor/PhabricatorAuthMessageEditor.php',
'PhabricatorAuthMessageListController' => 'applications/auth/controller/message/PhabricatorAuthMessageListController.php',
'PhabricatorAuthMessagePHIDType' => 'applications/auth/phid/PhabricatorAuthMessagePHIDType.php',
'PhabricatorAuthMessageQuery' => 'applications/auth/query/PhabricatorAuthMessageQuery.php',
'PhabricatorAuthMessageTextTransaction' => 'applications/auth/xaction/PhabricatorAuthMessageTextTransaction.php',
'PhabricatorAuthMessageTransaction' => 'applications/auth/storage/PhabricatorAuthMessageTransaction.php',
'PhabricatorAuthMessageTransactionQuery' => 'applications/auth/query/PhabricatorAuthMessageTransactionQuery.php',
'PhabricatorAuthMessageTransactionType' => 'applications/auth/xaction/PhabricatorAuthMessageTransactionType.php',
'PhabricatorAuthMessageType' => 'applications/auth/message/PhabricatorAuthMessageType.php',
'PhabricatorAuthMessageViewController' => 'applications/auth/controller/message/PhabricatorAuthMessageViewController.php',
'PhabricatorAuthNeedsApprovalController' => 'applications/auth/controller/PhabricatorAuthNeedsApprovalController.php',
'PhabricatorAuthNeedsMultiFactorController' => 'applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php',
'PhabricatorAuthNewController' => 'applications/auth/controller/config/PhabricatorAuthNewController.php',
'PhabricatorAuthNewFactorAction' => 'applications/auth/action/PhabricatorAuthNewFactorAction.php',
'PhabricatorAuthOldOAuthRedirectController' => 'applications/auth/controller/PhabricatorAuthOldOAuthRedirectController.php',
'PhabricatorAuthOneTimeLoginController' => 'applications/auth/controller/PhabricatorAuthOneTimeLoginController.php',
'PhabricatorAuthOneTimeLoginTemporaryTokenType' => 'applications/auth/tokentype/PhabricatorAuthOneTimeLoginTemporaryTokenType.php',
'PhabricatorAuthPassword' => 'applications/auth/storage/PhabricatorAuthPassword.php',
'PhabricatorAuthPasswordEditor' => 'applications/auth/editor/PhabricatorAuthPasswordEditor.php',
'PhabricatorAuthPasswordEngine' => 'applications/auth/engine/PhabricatorAuthPasswordEngine.php',
'PhabricatorAuthPasswordException' => 'applications/auth/password/PhabricatorAuthPasswordException.php',
'PhabricatorAuthPasswordHashInterface' => 'applications/auth/password/PhabricatorAuthPasswordHashInterface.php',
'PhabricatorAuthPasswordPHIDType' => 'applications/auth/phid/PhabricatorAuthPasswordPHIDType.php',
'PhabricatorAuthPasswordQuery' => 'applications/auth/query/PhabricatorAuthPasswordQuery.php',
'PhabricatorAuthPasswordResetTemporaryTokenType' => 'applications/auth/tokentype/PhabricatorAuthPasswordResetTemporaryTokenType.php',
'PhabricatorAuthPasswordRevokeTransaction' => 'applications/auth/xaction/PhabricatorAuthPasswordRevokeTransaction.php',
'PhabricatorAuthPasswordRevoker' => 'applications/auth/revoker/PhabricatorAuthPasswordRevoker.php',
'PhabricatorAuthPasswordTestCase' => 'applications/auth/__tests__/PhabricatorAuthPasswordTestCase.php',
'PhabricatorAuthPasswordTransaction' => 'applications/auth/storage/PhabricatorAuthPasswordTransaction.php',
'PhabricatorAuthPasswordTransactionQuery' => 'applications/auth/query/PhabricatorAuthPasswordTransactionQuery.php',
'PhabricatorAuthPasswordTransactionType' => 'applications/auth/xaction/PhabricatorAuthPasswordTransactionType.php',
'PhabricatorAuthPasswordUpgradeTransaction' => 'applications/auth/xaction/PhabricatorAuthPasswordUpgradeTransaction.php',
'PhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorAuthProvider.php',
'PhabricatorAuthProviderConfig' => 'applications/auth/storage/PhabricatorAuthProviderConfig.php',
'PhabricatorAuthProviderConfigController' => 'applications/auth/controller/config/PhabricatorAuthProviderConfigController.php',
'PhabricatorAuthProviderConfigEditor' => 'applications/auth/editor/PhabricatorAuthProviderConfigEditor.php',
'PhabricatorAuthProviderConfigQuery' => 'applications/auth/query/PhabricatorAuthProviderConfigQuery.php',
'PhabricatorAuthProviderConfigTransaction' => 'applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php',
'PhabricatorAuthProviderConfigTransactionQuery' => 'applications/auth/query/PhabricatorAuthProviderConfigTransactionQuery.php',
'PhabricatorAuthProviderController' => 'applications/auth/controller/config/PhabricatorAuthProviderController.php',
+ 'PhabricatorAuthProviderViewController' => 'applications/auth/controller/config/PhabricatorAuthProviderViewController.php',
'PhabricatorAuthProvidersGuidanceContext' => 'applications/auth/guidance/PhabricatorAuthProvidersGuidanceContext.php',
'PhabricatorAuthProvidersGuidanceEngineExtension' => 'applications/auth/guidance/PhabricatorAuthProvidersGuidanceEngineExtension.php',
'PhabricatorAuthQueryPublicKeysConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthQueryPublicKeysConduitAPIMethod.php',
'PhabricatorAuthRegisterController' => 'applications/auth/controller/PhabricatorAuthRegisterController.php',
'PhabricatorAuthRevokeTokenController' => 'applications/auth/controller/PhabricatorAuthRevokeTokenController.php',
'PhabricatorAuthRevoker' => 'applications/auth/revoker/PhabricatorAuthRevoker.php',
'PhabricatorAuthSSHKey' => 'applications/auth/storage/PhabricatorAuthSSHKey.php',
'PhabricatorAuthSSHKeyController' => 'applications/auth/controller/PhabricatorAuthSSHKeyController.php',
'PhabricatorAuthSSHKeyEditController' => 'applications/auth/controller/PhabricatorAuthSSHKeyEditController.php',
'PhabricatorAuthSSHKeyEditor' => 'applications/auth/editor/PhabricatorAuthSSHKeyEditor.php',
'PhabricatorAuthSSHKeyGenerateController' => 'applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php',
'PhabricatorAuthSSHKeyListController' => 'applications/auth/controller/PhabricatorAuthSSHKeyListController.php',
'PhabricatorAuthSSHKeyPHIDType' => 'applications/auth/phid/PhabricatorAuthSSHKeyPHIDType.php',
'PhabricatorAuthSSHKeyQuery' => 'applications/auth/query/PhabricatorAuthSSHKeyQuery.php',
'PhabricatorAuthSSHKeyReplyHandler' => 'applications/auth/mail/PhabricatorAuthSSHKeyReplyHandler.php',
'PhabricatorAuthSSHKeyRevokeController' => 'applications/auth/controller/PhabricatorAuthSSHKeyRevokeController.php',
'PhabricatorAuthSSHKeySearchEngine' => 'applications/auth/query/PhabricatorAuthSSHKeySearchEngine.php',
'PhabricatorAuthSSHKeyTableView' => 'applications/auth/view/PhabricatorAuthSSHKeyTableView.php',
'PhabricatorAuthSSHKeyTestCase' => 'applications/auth/__tests__/PhabricatorAuthSSHKeyTestCase.php',
'PhabricatorAuthSSHKeyTransaction' => 'applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php',
'PhabricatorAuthSSHKeyTransactionQuery' => 'applications/auth/query/PhabricatorAuthSSHKeyTransactionQuery.php',
'PhabricatorAuthSSHKeyViewController' => 'applications/auth/controller/PhabricatorAuthSSHKeyViewController.php',
'PhabricatorAuthSSHPublicKey' => 'applications/auth/sshkey/PhabricatorAuthSSHPublicKey.php',
'PhabricatorAuthSSHRevoker' => 'applications/auth/revoker/PhabricatorAuthSSHRevoker.php',
'PhabricatorAuthSession' => 'applications/auth/storage/PhabricatorAuthSession.php',
'PhabricatorAuthSessionEngine' => 'applications/auth/engine/PhabricatorAuthSessionEngine.php',
'PhabricatorAuthSessionEngineExtension' => 'applications/auth/engine/PhabricatorAuthSessionEngineExtension.php',
'PhabricatorAuthSessionEngineExtensionModule' => 'applications/auth/engine/PhabricatorAuthSessionEngineExtensionModule.php',
'PhabricatorAuthSessionGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php',
'PhabricatorAuthSessionInfo' => 'applications/auth/data/PhabricatorAuthSessionInfo.php',
'PhabricatorAuthSessionPHIDType' => 'applications/auth/phid/PhabricatorAuthSessionPHIDType.php',
'PhabricatorAuthSessionQuery' => 'applications/auth/query/PhabricatorAuthSessionQuery.php',
'PhabricatorAuthSessionRevoker' => 'applications/auth/revoker/PhabricatorAuthSessionRevoker.php',
+ 'PhabricatorAuthSetExternalController' => 'applications/auth/controller/PhabricatorAuthSetExternalController.php',
'PhabricatorAuthSetPasswordController' => 'applications/auth/controller/PhabricatorAuthSetPasswordController.php',
'PhabricatorAuthSetupCheck' => 'applications/config/check/PhabricatorAuthSetupCheck.php',
'PhabricatorAuthStartController' => 'applications/auth/controller/PhabricatorAuthStartController.php',
'PhabricatorAuthTemporaryToken' => 'applications/auth/storage/PhabricatorAuthTemporaryToken.php',
'PhabricatorAuthTemporaryTokenGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthTemporaryTokenGarbageCollector.php',
'PhabricatorAuthTemporaryTokenQuery' => 'applications/auth/query/PhabricatorAuthTemporaryTokenQuery.php',
'PhabricatorAuthTemporaryTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthTemporaryTokenRevoker.php',
'PhabricatorAuthTemporaryTokenType' => 'applications/auth/tokentype/PhabricatorAuthTemporaryTokenType.php',
'PhabricatorAuthTemporaryTokenTypeModule' => 'applications/auth/tokentype/PhabricatorAuthTemporaryTokenTypeModule.php',
'PhabricatorAuthTerminateSessionController' => 'applications/auth/controller/PhabricatorAuthTerminateSessionController.php',
'PhabricatorAuthTestSMSAction' => 'applications/auth/action/PhabricatorAuthTestSMSAction.php',
'PhabricatorAuthTryFactorAction' => 'applications/auth/action/PhabricatorAuthTryFactorAction.php',
'PhabricatorAuthUnlinkController' => 'applications/auth/controller/PhabricatorAuthUnlinkController.php',
'PhabricatorAuthValidateController' => 'applications/auth/controller/PhabricatorAuthValidateController.php',
'PhabricatorAuthWelcomeMailMessageType' => 'applications/auth/message/PhabricatorAuthWelcomeMailMessageType.php',
'PhabricatorAuthenticationConfigOptions' => 'applications/config/option/PhabricatorAuthenticationConfigOptions.php',
'PhabricatorAutoEventListener' => 'infrastructure/events/PhabricatorAutoEventListener.php',
'PhabricatorBadgesApplication' => 'applications/badges/application/PhabricatorBadgesApplication.php',
'PhabricatorBadgesArchiveController' => 'applications/badges/controller/PhabricatorBadgesArchiveController.php',
'PhabricatorBadgesAward' => 'applications/badges/storage/PhabricatorBadgesAward.php',
'PhabricatorBadgesAwardController' => 'applications/badges/controller/PhabricatorBadgesAwardController.php',
'PhabricatorBadgesAwardQuery' => 'applications/badges/query/PhabricatorBadgesAwardQuery.php',
'PhabricatorBadgesAwardTestDataGenerator' => 'applications/badges/lipsum/PhabricatorBadgesAwardTestDataGenerator.php',
'PhabricatorBadgesBadge' => 'applications/badges/storage/PhabricatorBadgesBadge.php',
'PhabricatorBadgesBadgeAwardTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeAwardTransaction.php',
'PhabricatorBadgesBadgeDescriptionTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeDescriptionTransaction.php',
'PhabricatorBadgesBadgeFlavorTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeFlavorTransaction.php',
'PhabricatorBadgesBadgeIconTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeIconTransaction.php',
'PhabricatorBadgesBadgeNameNgrams' => 'applications/badges/storage/PhabricatorBadgesBadgeNameNgrams.php',
'PhabricatorBadgesBadgeNameTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeNameTransaction.php',
'PhabricatorBadgesBadgeQualityTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeQualityTransaction.php',
'PhabricatorBadgesBadgeRevokeTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeRevokeTransaction.php',
'PhabricatorBadgesBadgeStatusTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeStatusTransaction.php',
'PhabricatorBadgesBadgeTestDataGenerator' => 'applications/badges/lipsum/PhabricatorBadgesBadgeTestDataGenerator.php',
'PhabricatorBadgesBadgeTransactionType' => 'applications/badges/xaction/PhabricatorBadgesBadgeTransactionType.php',
'PhabricatorBadgesCommentController' => 'applications/badges/controller/PhabricatorBadgesCommentController.php',
'PhabricatorBadgesController' => 'applications/badges/controller/PhabricatorBadgesController.php',
'PhabricatorBadgesCreateCapability' => 'applications/badges/capability/PhabricatorBadgesCreateCapability.php',
'PhabricatorBadgesDAO' => 'applications/badges/storage/PhabricatorBadgesDAO.php',
'PhabricatorBadgesDatasource' => 'applications/badges/typeahead/PhabricatorBadgesDatasource.php',
'PhabricatorBadgesDefaultEditCapability' => 'applications/badges/capability/PhabricatorBadgesDefaultEditCapability.php',
'PhabricatorBadgesEditConduitAPIMethod' => 'applications/badges/conduit/PhabricatorBadgesEditConduitAPIMethod.php',
'PhabricatorBadgesEditController' => 'applications/badges/controller/PhabricatorBadgesEditController.php',
'PhabricatorBadgesEditEngine' => 'applications/badges/editor/PhabricatorBadgesEditEngine.php',
'PhabricatorBadgesEditRecipientsController' => 'applications/badges/controller/PhabricatorBadgesEditRecipientsController.php',
'PhabricatorBadgesEditor' => 'applications/badges/editor/PhabricatorBadgesEditor.php',
'PhabricatorBadgesIconSet' => 'applications/badges/icon/PhabricatorBadgesIconSet.php',
'PhabricatorBadgesListController' => 'applications/badges/controller/PhabricatorBadgesListController.php',
'PhabricatorBadgesLootContextFreeGrammar' => 'applications/badges/lipsum/PhabricatorBadgesLootContextFreeGrammar.php',
'PhabricatorBadgesMailReceiver' => 'applications/badges/mail/PhabricatorBadgesMailReceiver.php',
'PhabricatorBadgesPHIDType' => 'applications/badges/phid/PhabricatorBadgesPHIDType.php',
'PhabricatorBadgesProfileController' => 'applications/badges/controller/PhabricatorBadgesProfileController.php',
'PhabricatorBadgesQuality' => 'applications/badges/constants/PhabricatorBadgesQuality.php',
'PhabricatorBadgesQuery' => 'applications/badges/query/PhabricatorBadgesQuery.php',
'PhabricatorBadgesRecipientsController' => 'applications/badges/controller/PhabricatorBadgesRecipientsController.php',
'PhabricatorBadgesRecipientsListView' => 'applications/badges/view/PhabricatorBadgesRecipientsListView.php',
'PhabricatorBadgesRemoveRecipientsController' => 'applications/badges/controller/PhabricatorBadgesRemoveRecipientsController.php',
'PhabricatorBadgesReplyHandler' => 'applications/badges/mail/PhabricatorBadgesReplyHandler.php',
'PhabricatorBadgesSchemaSpec' => 'applications/badges/storage/PhabricatorBadgesSchemaSpec.php',
'PhabricatorBadgesSearchConduitAPIMethod' => 'applications/badges/conduit/PhabricatorBadgesSearchConduitAPIMethod.php',
'PhabricatorBadgesSearchEngine' => 'applications/badges/query/PhabricatorBadgesSearchEngine.php',
'PhabricatorBadgesTransaction' => 'applications/badges/storage/PhabricatorBadgesTransaction.php',
'PhabricatorBadgesTransactionComment' => 'applications/badges/storage/PhabricatorBadgesTransactionComment.php',
'PhabricatorBadgesTransactionQuery' => 'applications/badges/query/PhabricatorBadgesTransactionQuery.php',
'PhabricatorBadgesViewController' => 'applications/badges/controller/PhabricatorBadgesViewController.php',
'PhabricatorBarePageView' => 'view/page/PhabricatorBarePageView.php',
'PhabricatorBaseURISetupCheck' => 'applications/config/check/PhabricatorBaseURISetupCheck.php',
'PhabricatorBcryptPasswordHasher' => 'infrastructure/util/password/PhabricatorBcryptPasswordHasher.php',
'PhabricatorBinariesSetupCheck' => 'applications/config/check/PhabricatorBinariesSetupCheck.php',
'PhabricatorBitbucketAuthProvider' => 'applications/auth/provider/PhabricatorBitbucketAuthProvider.php',
'PhabricatorBoardColumnsSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorBoardColumnsSearchEngineAttachment.php',
'PhabricatorBoardLayoutEngine' => 'applications/project/engine/PhabricatorBoardLayoutEngine.php',
'PhabricatorBoardRenderingEngine' => 'applications/project/engine/PhabricatorBoardRenderingEngine.php',
'PhabricatorBoardResponseEngine' => 'applications/project/engine/PhabricatorBoardResponseEngine.php',
'PhabricatorBoolConfigType' => 'applications/config/type/PhabricatorBoolConfigType.php',
'PhabricatorBoolEditField' => 'applications/transactions/editfield/PhabricatorBoolEditField.php',
'PhabricatorBoolMailStamp' => 'applications/metamta/stamp/PhabricatorBoolMailStamp.php',
'PhabricatorBritishEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorBritishEnglishTranslation.php',
'PhabricatorBuiltinDraftEngine' => 'applications/transactions/draft/PhabricatorBuiltinDraftEngine.php',
'PhabricatorBuiltinFileCachePurger' => 'applications/cache/purger/PhabricatorBuiltinFileCachePurger.php',
'PhabricatorBuiltinPatchList' => 'infrastructure/storage/patch/PhabricatorBuiltinPatchList.php',
'PhabricatorBulkContentSource' => 'infrastructure/daemon/contentsource/PhabricatorBulkContentSource.php',
'PhabricatorBulkEditGroup' => 'applications/transactions/bulk/PhabricatorBulkEditGroup.php',
'PhabricatorBulkEngine' => 'applications/transactions/bulk/PhabricatorBulkEngine.php',
'PhabricatorBulkManagementExportWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementExportWorkflow.php',
'PhabricatorBulkManagementMakeSilentWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementMakeSilentWorkflow.php',
'PhabricatorBulkManagementWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementWorkflow.php',
'PhabricatorCSVExportFormat' => 'infrastructure/export/format/PhabricatorCSVExportFormat.php',
'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.php',
'PhabricatorCacheEngine' => 'applications/system/engine/PhabricatorCacheEngine.php',
'PhabricatorCacheEngineExtension' => 'applications/system/engine/PhabricatorCacheEngineExtension.php',
'PhabricatorCacheGeneralGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheGeneralGarbageCollector.php',
'PhabricatorCacheManagementPurgeWorkflow' => 'applications/cache/management/PhabricatorCacheManagementPurgeWorkflow.php',
'PhabricatorCacheManagementWorkflow' => 'applications/cache/management/PhabricatorCacheManagementWorkflow.php',
'PhabricatorCacheMarkupGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheMarkupGarbageCollector.php',
'PhabricatorCachePurger' => 'applications/cache/purger/PhabricatorCachePurger.php',
'PhabricatorCacheSchemaSpec' => 'applications/cache/storage/PhabricatorCacheSchemaSpec.php',
'PhabricatorCacheSetupCheck' => 'applications/config/check/PhabricatorCacheSetupCheck.php',
'PhabricatorCacheSpec' => 'applications/cache/spec/PhabricatorCacheSpec.php',
'PhabricatorCacheTTLGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheTTLGarbageCollector.php',
'PhabricatorCachedClassMapQuery' => 'applications/cache/PhabricatorCachedClassMapQuery.php',
'PhabricatorCaches' => 'applications/cache/PhabricatorCaches.php',
'PhabricatorCachesTestCase' => 'applications/cache/__tests__/PhabricatorCachesTestCase.php',
'PhabricatorCalendarApplication' => 'applications/calendar/application/PhabricatorCalendarApplication.php',
'PhabricatorCalendarController' => 'applications/calendar/controller/PhabricatorCalendarController.php',
'PhabricatorCalendarDAO' => 'applications/calendar/storage/PhabricatorCalendarDAO.php',
'PhabricatorCalendarEvent' => 'applications/calendar/storage/PhabricatorCalendarEvent.php',
'PhabricatorCalendarEventAcceptTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventAcceptTransaction.php',
'PhabricatorCalendarEventAllDayTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventAllDayTransaction.php',
'PhabricatorCalendarEventAvailabilityController' => 'applications/calendar/controller/PhabricatorCalendarEventAvailabilityController.php',
'PhabricatorCalendarEventCancelController' => 'applications/calendar/controller/PhabricatorCalendarEventCancelController.php',
'PhabricatorCalendarEventCancelTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventCancelTransaction.php',
'PhabricatorCalendarEventDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventDateTransaction.php',
'PhabricatorCalendarEventDeclineTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventDeclineTransaction.php',
'PhabricatorCalendarEventDefaultEditCapability' => 'applications/calendar/capability/PhabricatorCalendarEventDefaultEditCapability.php',
'PhabricatorCalendarEventDefaultViewCapability' => 'applications/calendar/capability/PhabricatorCalendarEventDefaultViewCapability.php',
'PhabricatorCalendarEventDescriptionTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventDescriptionTransaction.php',
'PhabricatorCalendarEventDragController' => 'applications/calendar/controller/PhabricatorCalendarEventDragController.php',
'PhabricatorCalendarEventEditConduitAPIMethod' => 'applications/calendar/conduit/PhabricatorCalendarEventEditConduitAPIMethod.php',
'PhabricatorCalendarEventEditController' => 'applications/calendar/controller/PhabricatorCalendarEventEditController.php',
'PhabricatorCalendarEventEditEngine' => 'applications/calendar/editor/PhabricatorCalendarEventEditEngine.php',
'PhabricatorCalendarEventEditor' => 'applications/calendar/editor/PhabricatorCalendarEventEditor.php',
'PhabricatorCalendarEventEmailCommand' => 'applications/calendar/command/PhabricatorCalendarEventEmailCommand.php',
'PhabricatorCalendarEventEndDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventEndDateTransaction.php',
'PhabricatorCalendarEventExportController' => 'applications/calendar/controller/PhabricatorCalendarEventExportController.php',
'PhabricatorCalendarEventFerretEngine' => 'applications/calendar/search/PhabricatorCalendarEventFerretEngine.php',
'PhabricatorCalendarEventForkTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventForkTransaction.php',
'PhabricatorCalendarEventFrequencyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventFrequencyTransaction.php',
'PhabricatorCalendarEventFulltextEngine' => 'applications/calendar/search/PhabricatorCalendarEventFulltextEngine.php',
'PhabricatorCalendarEventHeraldAdapter' => 'applications/calendar/herald/PhabricatorCalendarEventHeraldAdapter.php',
'PhabricatorCalendarEventHeraldField' => 'applications/calendar/herald/PhabricatorCalendarEventHeraldField.php',
'PhabricatorCalendarEventHeraldFieldGroup' => 'applications/calendar/herald/PhabricatorCalendarEventHeraldFieldGroup.php',
'PhabricatorCalendarEventHostPolicyRule' => 'applications/calendar/policyrule/PhabricatorCalendarEventHostPolicyRule.php',
'PhabricatorCalendarEventHostTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventHostTransaction.php',
'PhabricatorCalendarEventIconTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventIconTransaction.php',
'PhabricatorCalendarEventInviteTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventInviteTransaction.php',
'PhabricatorCalendarEventInvitee' => 'applications/calendar/storage/PhabricatorCalendarEventInvitee.php',
'PhabricatorCalendarEventInviteeQuery' => 'applications/calendar/query/PhabricatorCalendarEventInviteeQuery.php',
'PhabricatorCalendarEventInviteesPolicyRule' => 'applications/calendar/policyrule/PhabricatorCalendarEventInviteesPolicyRule.php',
'PhabricatorCalendarEventJoinController' => 'applications/calendar/controller/PhabricatorCalendarEventJoinController.php',
'PhabricatorCalendarEventListController' => 'applications/calendar/controller/PhabricatorCalendarEventListController.php',
'PhabricatorCalendarEventMailReceiver' => 'applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php',
'PhabricatorCalendarEventNameHeraldField' => 'applications/calendar/herald/PhabricatorCalendarEventNameHeraldField.php',
'PhabricatorCalendarEventNameTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventNameTransaction.php',
'PhabricatorCalendarEventNotificationView' => 'applications/calendar/notifications/PhabricatorCalendarEventNotificationView.php',
'PhabricatorCalendarEventPHIDType' => 'applications/calendar/phid/PhabricatorCalendarEventPHIDType.php',
'PhabricatorCalendarEventPolicyCodex' => 'applications/calendar/codex/PhabricatorCalendarEventPolicyCodex.php',
'PhabricatorCalendarEventQuery' => 'applications/calendar/query/PhabricatorCalendarEventQuery.php',
'PhabricatorCalendarEventRSVPEmailCommand' => 'applications/calendar/command/PhabricatorCalendarEventRSVPEmailCommand.php',
'PhabricatorCalendarEventRecurringTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventRecurringTransaction.php',
'PhabricatorCalendarEventReplyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventReplyTransaction.php',
'PhabricatorCalendarEventSearchConduitAPIMethod' => 'applications/calendar/conduit/PhabricatorCalendarEventSearchConduitAPIMethod.php',
'PhabricatorCalendarEventSearchEngine' => 'applications/calendar/query/PhabricatorCalendarEventSearchEngine.php',
'PhabricatorCalendarEventStartDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventStartDateTransaction.php',
'PhabricatorCalendarEventTransaction' => 'applications/calendar/storage/PhabricatorCalendarEventTransaction.php',
'PhabricatorCalendarEventTransactionComment' => 'applications/calendar/storage/PhabricatorCalendarEventTransactionComment.php',
'PhabricatorCalendarEventTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarEventTransactionQuery.php',
'PhabricatorCalendarEventTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarEventTransactionType.php',
'PhabricatorCalendarEventUntilDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventUntilDateTransaction.php',
'PhabricatorCalendarEventViewController' => 'applications/calendar/controller/PhabricatorCalendarEventViewController.php',
'PhabricatorCalendarExport' => 'applications/calendar/storage/PhabricatorCalendarExport.php',
'PhabricatorCalendarExportDisableController' => 'applications/calendar/controller/PhabricatorCalendarExportDisableController.php',
'PhabricatorCalendarExportDisableTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportDisableTransaction.php',
'PhabricatorCalendarExportEditController' => 'applications/calendar/controller/PhabricatorCalendarExportEditController.php',
'PhabricatorCalendarExportEditEngine' => 'applications/calendar/editor/PhabricatorCalendarExportEditEngine.php',
'PhabricatorCalendarExportEditor' => 'applications/calendar/editor/PhabricatorCalendarExportEditor.php',
'PhabricatorCalendarExportICSController' => 'applications/calendar/controller/PhabricatorCalendarExportICSController.php',
'PhabricatorCalendarExportListController' => 'applications/calendar/controller/PhabricatorCalendarExportListController.php',
'PhabricatorCalendarExportModeTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportModeTransaction.php',
'PhabricatorCalendarExportNameTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportNameTransaction.php',
'PhabricatorCalendarExportPHIDType' => 'applications/calendar/phid/PhabricatorCalendarExportPHIDType.php',
'PhabricatorCalendarExportQuery' => 'applications/calendar/query/PhabricatorCalendarExportQuery.php',
'PhabricatorCalendarExportQueryKeyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportQueryKeyTransaction.php',
'PhabricatorCalendarExportSearchEngine' => 'applications/calendar/query/PhabricatorCalendarExportSearchEngine.php',
'PhabricatorCalendarExportTransaction' => 'applications/calendar/storage/PhabricatorCalendarExportTransaction.php',
'PhabricatorCalendarExportTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarExportTransactionQuery.php',
'PhabricatorCalendarExportTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarExportTransactionType.php',
'PhabricatorCalendarExportViewController' => 'applications/calendar/controller/PhabricatorCalendarExportViewController.php',
'PhabricatorCalendarExternalInvitee' => 'applications/calendar/storage/PhabricatorCalendarExternalInvitee.php',
'PhabricatorCalendarExternalInviteePHIDType' => 'applications/calendar/phid/PhabricatorCalendarExternalInviteePHIDType.php',
'PhabricatorCalendarExternalInviteeQuery' => 'applications/calendar/query/PhabricatorCalendarExternalInviteeQuery.php',
'PhabricatorCalendarICSFileImportEngine' => 'applications/calendar/import/PhabricatorCalendarICSFileImportEngine.php',
'PhabricatorCalendarICSImportEngine' => 'applications/calendar/import/PhabricatorCalendarICSImportEngine.php',
'PhabricatorCalendarICSURIImportEngine' => 'applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php',
'PhabricatorCalendarICSWriter' => 'applications/calendar/util/PhabricatorCalendarICSWriter.php',
'PhabricatorCalendarIconSet' => 'applications/calendar/icon/PhabricatorCalendarIconSet.php',
'PhabricatorCalendarImport' => 'applications/calendar/storage/PhabricatorCalendarImport.php',
'PhabricatorCalendarImportDefaultLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportDefaultLogType.php',
'PhabricatorCalendarImportDeleteController' => 'applications/calendar/controller/PhabricatorCalendarImportDeleteController.php',
'PhabricatorCalendarImportDeleteLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportDeleteLogType.php',
'PhabricatorCalendarImportDeleteTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportDeleteTransaction.php',
'PhabricatorCalendarImportDisableController' => 'applications/calendar/controller/PhabricatorCalendarImportDisableController.php',
'PhabricatorCalendarImportDisableTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportDisableTransaction.php',
'PhabricatorCalendarImportDropController' => 'applications/calendar/controller/PhabricatorCalendarImportDropController.php',
'PhabricatorCalendarImportDuplicateLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportDuplicateLogType.php',
'PhabricatorCalendarImportEditController' => 'applications/calendar/controller/PhabricatorCalendarImportEditController.php',
'PhabricatorCalendarImportEditEngine' => 'applications/calendar/editor/PhabricatorCalendarImportEditEngine.php',
'PhabricatorCalendarImportEditor' => 'applications/calendar/editor/PhabricatorCalendarImportEditor.php',
'PhabricatorCalendarImportEmptyLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportEmptyLogType.php',
'PhabricatorCalendarImportEngine' => 'applications/calendar/import/PhabricatorCalendarImportEngine.php',
'PhabricatorCalendarImportEpochLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportEpochLogType.php',
'PhabricatorCalendarImportFetchLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportFetchLogType.php',
'PhabricatorCalendarImportFrequencyLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportFrequencyLogType.php',
'PhabricatorCalendarImportFrequencyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportFrequencyTransaction.php',
'PhabricatorCalendarImportICSFileTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportICSFileTransaction.php',
'PhabricatorCalendarImportICSLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportICSLogType.php',
'PhabricatorCalendarImportICSURITransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportICSURITransaction.php',
'PhabricatorCalendarImportICSWarningLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportICSWarningLogType.php',
'PhabricatorCalendarImportIgnoredNodeLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportIgnoredNodeLogType.php',
'PhabricatorCalendarImportListController' => 'applications/calendar/controller/PhabricatorCalendarImportListController.php',
'PhabricatorCalendarImportLog' => 'applications/calendar/storage/PhabricatorCalendarImportLog.php',
'PhabricatorCalendarImportLogListController' => 'applications/calendar/controller/PhabricatorCalendarImportLogListController.php',
'PhabricatorCalendarImportLogQuery' => 'applications/calendar/query/PhabricatorCalendarImportLogQuery.php',
'PhabricatorCalendarImportLogSearchEngine' => 'applications/calendar/query/PhabricatorCalendarImportLogSearchEngine.php',
'PhabricatorCalendarImportLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportLogType.php',
'PhabricatorCalendarImportLogView' => 'applications/calendar/view/PhabricatorCalendarImportLogView.php',
'PhabricatorCalendarImportNameTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportNameTransaction.php',
'PhabricatorCalendarImportOriginalLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportOriginalLogType.php',
'PhabricatorCalendarImportOrphanLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportOrphanLogType.php',
'PhabricatorCalendarImportPHIDType' => 'applications/calendar/phid/PhabricatorCalendarImportPHIDType.php',
'PhabricatorCalendarImportQuery' => 'applications/calendar/query/PhabricatorCalendarImportQuery.php',
'PhabricatorCalendarImportQueueLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportQueueLogType.php',
'PhabricatorCalendarImportReloadController' => 'applications/calendar/controller/PhabricatorCalendarImportReloadController.php',
'PhabricatorCalendarImportReloadTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportReloadTransaction.php',
'PhabricatorCalendarImportReloadWorker' => 'applications/calendar/worker/PhabricatorCalendarImportReloadWorker.php',
'PhabricatorCalendarImportSearchEngine' => 'applications/calendar/query/PhabricatorCalendarImportSearchEngine.php',
'PhabricatorCalendarImportTransaction' => 'applications/calendar/storage/PhabricatorCalendarImportTransaction.php',
'PhabricatorCalendarImportTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarImportTransactionQuery.php',
'PhabricatorCalendarImportTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarImportTransactionType.php',
'PhabricatorCalendarImportTriggerLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportTriggerLogType.php',
'PhabricatorCalendarImportUpdateLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportUpdateLogType.php',
'PhabricatorCalendarImportViewController' => 'applications/calendar/controller/PhabricatorCalendarImportViewController.php',
'PhabricatorCalendarInviteeDatasource' => 'applications/calendar/typeahead/PhabricatorCalendarInviteeDatasource.php',
'PhabricatorCalendarInviteeUserDatasource' => 'applications/calendar/typeahead/PhabricatorCalendarInviteeUserDatasource.php',
'PhabricatorCalendarInviteeViewerFunctionDatasource' => 'applications/calendar/typeahead/PhabricatorCalendarInviteeViewerFunctionDatasource.php',
'PhabricatorCalendarManagementNotifyWorkflow' => 'applications/calendar/management/PhabricatorCalendarManagementNotifyWorkflow.php',
'PhabricatorCalendarManagementReloadWorkflow' => 'applications/calendar/management/PhabricatorCalendarManagementReloadWorkflow.php',
'PhabricatorCalendarManagementWorkflow' => 'applications/calendar/management/PhabricatorCalendarManagementWorkflow.php',
'PhabricatorCalendarNotification' => 'applications/calendar/storage/PhabricatorCalendarNotification.php',
'PhabricatorCalendarNotificationEngine' => 'applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php',
'PhabricatorCalendarRemarkupRule' => 'applications/calendar/remarkup/PhabricatorCalendarRemarkupRule.php',
'PhabricatorCalendarReplyHandler' => 'applications/calendar/mail/PhabricatorCalendarReplyHandler.php',
'PhabricatorCalendarSchemaSpec' => 'applications/calendar/storage/PhabricatorCalendarSchemaSpec.php',
'PhabricatorCelerityApplication' => 'applications/celerity/application/PhabricatorCelerityApplication.php',
'PhabricatorCelerityTestCase' => '__tests__/PhabricatorCelerityTestCase.php',
'PhabricatorChangeParserTestCase' => 'applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php',
'PhabricatorChangesetCachePurger' => 'applications/cache/purger/PhabricatorChangesetCachePurger.php',
'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php',
'PhabricatorChatLogApplication' => 'applications/chatlog/application/PhabricatorChatLogApplication.php',
'PhabricatorChatLogChannel' => 'applications/chatlog/storage/PhabricatorChatLogChannel.php',
'PhabricatorChatLogChannelListController' => 'applications/chatlog/controller/PhabricatorChatLogChannelListController.php',
'PhabricatorChatLogChannelLogController' => 'applications/chatlog/controller/PhabricatorChatLogChannelLogController.php',
'PhabricatorChatLogChannelQuery' => 'applications/chatlog/query/PhabricatorChatLogChannelQuery.php',
'PhabricatorChatLogController' => 'applications/chatlog/controller/PhabricatorChatLogController.php',
'PhabricatorChatLogDAO' => 'applications/chatlog/storage/PhabricatorChatLogDAO.php',
'PhabricatorChatLogEvent' => 'applications/chatlog/storage/PhabricatorChatLogEvent.php',
'PhabricatorChatLogQuery' => 'applications/chatlog/query/PhabricatorChatLogQuery.php',
'PhabricatorCheckboxesEditField' => 'applications/transactions/editfield/PhabricatorCheckboxesEditField.php',
'PhabricatorChunkedFileStorageEngine' => 'applications/files/engine/PhabricatorChunkedFileStorageEngine.php',
'PhabricatorClassConfigType' => 'applications/config/type/PhabricatorClassConfigType.php',
'PhabricatorClusterConfigOptions' => 'applications/config/option/PhabricatorClusterConfigOptions.php',
'PhabricatorClusterDatabasesConfigType' => 'infrastructure/cluster/config/PhabricatorClusterDatabasesConfigType.php',
'PhabricatorClusterException' => 'infrastructure/cluster/exception/PhabricatorClusterException.php',
'PhabricatorClusterExceptionHandler' => 'infrastructure/cluster/exception/PhabricatorClusterExceptionHandler.php',
'PhabricatorClusterImpossibleWriteException' => 'infrastructure/cluster/exception/PhabricatorClusterImpossibleWriteException.php',
'PhabricatorClusterImproperWriteException' => 'infrastructure/cluster/exception/PhabricatorClusterImproperWriteException.php',
'PhabricatorClusterMailersConfigType' => 'infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php',
'PhabricatorClusterNoHostForRoleException' => 'infrastructure/cluster/exception/PhabricatorClusterNoHostForRoleException.php',
'PhabricatorClusterSearchConfigType' => 'infrastructure/cluster/config/PhabricatorClusterSearchConfigType.php',
'PhabricatorClusterServiceHealthRecord' => 'infrastructure/cluster/PhabricatorClusterServiceHealthRecord.php',
'PhabricatorClusterStrandedException' => 'infrastructure/cluster/exception/PhabricatorClusterStrandedException.php',
'PhabricatorColumnProxyInterface' => 'applications/project/interface/PhabricatorColumnProxyInterface.php',
'PhabricatorColumnsEditField' => 'applications/transactions/editfield/PhabricatorColumnsEditField.php',
'PhabricatorCommentEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php',
'PhabricatorCommentEditField' => 'applications/transactions/editfield/PhabricatorCommentEditField.php',
'PhabricatorCommentEditType' => 'applications/transactions/edittype/PhabricatorCommentEditType.php',
'PhabricatorCommitBranchesField' => 'applications/repository/customfield/PhabricatorCommitBranchesField.php',
'PhabricatorCommitCustomField' => 'applications/repository/customfield/PhabricatorCommitCustomField.php',
'PhabricatorCommitMergedCommitsField' => 'applications/repository/customfield/PhabricatorCommitMergedCommitsField.php',
'PhabricatorCommitRepositoryField' => 'applications/repository/customfield/PhabricatorCommitRepositoryField.php',
'PhabricatorCommitSearchEngine' => 'applications/audit/query/PhabricatorCommitSearchEngine.php',
'PhabricatorCommitTagsField' => 'applications/repository/customfield/PhabricatorCommitTagsField.php',
'PhabricatorCommonPasswords' => 'applications/auth/constants/PhabricatorCommonPasswords.php',
'PhabricatorConduitAPIController' => 'applications/conduit/controller/PhabricatorConduitAPIController.php',
'PhabricatorConduitApplication' => 'applications/conduit/application/PhabricatorConduitApplication.php',
'PhabricatorConduitCallManagementWorkflow' => 'applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php',
'PhabricatorConduitCertificateToken' => 'applications/conduit/storage/PhabricatorConduitCertificateToken.php',
'PhabricatorConduitConsoleController' => 'applications/conduit/controller/PhabricatorConduitConsoleController.php',
'PhabricatorConduitContentSource' => 'infrastructure/contentsource/PhabricatorConduitContentSource.php',
'PhabricatorConduitController' => 'applications/conduit/controller/PhabricatorConduitController.php',
'PhabricatorConduitDAO' => 'applications/conduit/storage/PhabricatorConduitDAO.php',
'PhabricatorConduitEditField' => 'applications/transactions/editfield/PhabricatorConduitEditField.php',
'PhabricatorConduitListController' => 'applications/conduit/controller/PhabricatorConduitListController.php',
'PhabricatorConduitLogController' => 'applications/conduit/controller/PhabricatorConduitLogController.php',
'PhabricatorConduitLogQuery' => 'applications/conduit/query/PhabricatorConduitLogQuery.php',
'PhabricatorConduitLogSearchEngine' => 'applications/conduit/query/PhabricatorConduitLogSearchEngine.php',
'PhabricatorConduitManagementWorkflow' => 'applications/conduit/management/PhabricatorConduitManagementWorkflow.php',
'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/PhabricatorConduitMethodCallLog.php',
'PhabricatorConduitMethodQuery' => 'applications/conduit/query/PhabricatorConduitMethodQuery.php',
'PhabricatorConduitRequestExceptionHandler' => 'aphront/handler/PhabricatorConduitRequestExceptionHandler.php',
'PhabricatorConduitResultInterface' => 'applications/conduit/interface/PhabricatorConduitResultInterface.php',
'PhabricatorConduitSearchEngine' => 'applications/conduit/query/PhabricatorConduitSearchEngine.php',
'PhabricatorConduitSearchFieldSpecification' => 'applications/conduit/interface/PhabricatorConduitSearchFieldSpecification.php',
'PhabricatorConduitTestCase' => '__tests__/PhabricatorConduitTestCase.php',
'PhabricatorConduitToken' => 'applications/conduit/storage/PhabricatorConduitToken.php',
'PhabricatorConduitTokenController' => 'applications/conduit/controller/PhabricatorConduitTokenController.php',
'PhabricatorConduitTokenEditController' => 'applications/conduit/controller/PhabricatorConduitTokenEditController.php',
'PhabricatorConduitTokenHandshakeController' => 'applications/conduit/controller/PhabricatorConduitTokenHandshakeController.php',
'PhabricatorConduitTokenQuery' => 'applications/conduit/query/PhabricatorConduitTokenQuery.php',
'PhabricatorConduitTokenTerminateController' => 'applications/conduit/controller/PhabricatorConduitTokenTerminateController.php',
'PhabricatorConduitTokensSettingsPanel' => 'applications/conduit/settings/PhabricatorConduitTokensSettingsPanel.php',
'PhabricatorConfigAllController' => 'applications/config/controller/PhabricatorConfigAllController.php',
'PhabricatorConfigApplication' => 'applications/config/application/PhabricatorConfigApplication.php',
'PhabricatorConfigApplicationController' => 'applications/config/controller/PhabricatorConfigApplicationController.php',
'PhabricatorConfigCacheController' => 'applications/config/controller/PhabricatorConfigCacheController.php',
'PhabricatorConfigClusterDatabasesController' => 'applications/config/controller/PhabricatorConfigClusterDatabasesController.php',
'PhabricatorConfigClusterNotificationsController' => 'applications/config/controller/PhabricatorConfigClusterNotificationsController.php',
'PhabricatorConfigClusterRepositoriesController' => 'applications/config/controller/PhabricatorConfigClusterRepositoriesController.php',
'PhabricatorConfigClusterSearchController' => 'applications/config/controller/PhabricatorConfigClusterSearchController.php',
'PhabricatorConfigCollectorsModule' => 'applications/config/module/PhabricatorConfigCollectorsModule.php',
'PhabricatorConfigColumnSchema' => 'applications/config/schema/PhabricatorConfigColumnSchema.php',
'PhabricatorConfigConfigPHIDType' => 'applications/config/phid/PhabricatorConfigConfigPHIDType.php',
'PhabricatorConfigConstants' => 'applications/config/constants/PhabricatorConfigConstants.php',
'PhabricatorConfigController' => 'applications/config/controller/PhabricatorConfigController.php',
'PhabricatorConfigCoreSchemaSpec' => 'applications/config/schema/PhabricatorConfigCoreSchemaSpec.php',
'PhabricatorConfigDatabaseController' => 'applications/config/controller/PhabricatorConfigDatabaseController.php',
'PhabricatorConfigDatabaseIssueController' => 'applications/config/controller/PhabricatorConfigDatabaseIssueController.php',
'PhabricatorConfigDatabaseSchema' => 'applications/config/schema/PhabricatorConfigDatabaseSchema.php',
'PhabricatorConfigDatabaseSource' => 'infrastructure/env/PhabricatorConfigDatabaseSource.php',
'PhabricatorConfigDatabaseStatusController' => 'applications/config/controller/PhabricatorConfigDatabaseStatusController.php',
'PhabricatorConfigDefaultSource' => 'infrastructure/env/PhabricatorConfigDefaultSource.php',
'PhabricatorConfigDictionarySource' => 'infrastructure/env/PhabricatorConfigDictionarySource.php',
'PhabricatorConfigEdgeModule' => 'applications/config/module/PhabricatorConfigEdgeModule.php',
'PhabricatorConfigEditController' => 'applications/config/controller/PhabricatorConfigEditController.php',
'PhabricatorConfigEditor' => 'applications/config/editor/PhabricatorConfigEditor.php',
'PhabricatorConfigEntry' => 'applications/config/storage/PhabricatorConfigEntry.php',
'PhabricatorConfigEntryDAO' => 'applications/config/storage/PhabricatorConfigEntryDAO.php',
'PhabricatorConfigEntryQuery' => 'applications/config/query/PhabricatorConfigEntryQuery.php',
'PhabricatorConfigFileSource' => 'infrastructure/env/PhabricatorConfigFileSource.php',
'PhabricatorConfigGroupConstants' => 'applications/config/constants/PhabricatorConfigGroupConstants.php',
'PhabricatorConfigGroupController' => 'applications/config/controller/PhabricatorConfigGroupController.php',
'PhabricatorConfigHTTPParameterTypesModule' => 'applications/config/module/PhabricatorConfigHTTPParameterTypesModule.php',
'PhabricatorConfigHistoryController' => 'applications/config/controller/PhabricatorConfigHistoryController.php',
'PhabricatorConfigIgnoreController' => 'applications/config/controller/PhabricatorConfigIgnoreController.php',
'PhabricatorConfigIssueListController' => 'applications/config/controller/PhabricatorConfigIssueListController.php',
'PhabricatorConfigIssuePanelController' => 'applications/config/controller/PhabricatorConfigIssuePanelController.php',
'PhabricatorConfigIssueViewController' => 'applications/config/controller/PhabricatorConfigIssueViewController.php',
'PhabricatorConfigJSON' => 'applications/config/json/PhabricatorConfigJSON.php',
'PhabricatorConfigJSONOptionType' => 'applications/config/custom/PhabricatorConfigJSONOptionType.php',
'PhabricatorConfigKeySchema' => 'applications/config/schema/PhabricatorConfigKeySchema.php',
'PhabricatorConfigListController' => 'applications/config/controller/PhabricatorConfigListController.php',
'PhabricatorConfigLocalSource' => 'infrastructure/env/PhabricatorConfigLocalSource.php',
'PhabricatorConfigManagementDeleteWorkflow' => 'applications/config/management/PhabricatorConfigManagementDeleteWorkflow.php',
'PhabricatorConfigManagementDoneWorkflow' => 'applications/config/management/PhabricatorConfigManagementDoneWorkflow.php',
'PhabricatorConfigManagementGetWorkflow' => 'applications/config/management/PhabricatorConfigManagementGetWorkflow.php',
'PhabricatorConfigManagementListWorkflow' => 'applications/config/management/PhabricatorConfigManagementListWorkflow.php',
'PhabricatorConfigManagementMigrateWorkflow' => 'applications/config/management/PhabricatorConfigManagementMigrateWorkflow.php',
'PhabricatorConfigManagementSetWorkflow' => 'applications/config/management/PhabricatorConfigManagementSetWorkflow.php',
'PhabricatorConfigManagementWorkflow' => 'applications/config/management/PhabricatorConfigManagementWorkflow.php',
'PhabricatorConfigManualActivity' => 'applications/config/storage/PhabricatorConfigManualActivity.php',
'PhabricatorConfigModule' => 'applications/config/module/PhabricatorConfigModule.php',
'PhabricatorConfigModuleController' => 'applications/config/controller/PhabricatorConfigModuleController.php',
'PhabricatorConfigOption' => 'applications/config/option/PhabricatorConfigOption.php',
'PhabricatorConfigOptionType' => 'applications/config/custom/PhabricatorConfigOptionType.php',
'PhabricatorConfigPHIDModule' => 'applications/config/module/PhabricatorConfigPHIDModule.php',
'PhabricatorConfigProxySource' => 'infrastructure/env/PhabricatorConfigProxySource.php',
'PhabricatorConfigPurgeCacheController' => 'applications/config/controller/PhabricatorConfigPurgeCacheController.php',
'PhabricatorConfigRegexOptionType' => 'applications/config/custom/PhabricatorConfigRegexOptionType.php',
'PhabricatorConfigRemarkupRule' => 'infrastructure/markup/rule/PhabricatorConfigRemarkupRule.php',
'PhabricatorConfigRequestExceptionHandlerModule' => 'applications/config/module/PhabricatorConfigRequestExceptionHandlerModule.php',
'PhabricatorConfigResponse' => 'applications/config/response/PhabricatorConfigResponse.php',
'PhabricatorConfigSchemaQuery' => 'applications/config/schema/PhabricatorConfigSchemaQuery.php',
'PhabricatorConfigSchemaSpec' => 'applications/config/schema/PhabricatorConfigSchemaSpec.php',
'PhabricatorConfigServerSchema' => 'applications/config/schema/PhabricatorConfigServerSchema.php',
'PhabricatorConfigSetupCheckModule' => 'applications/config/module/PhabricatorConfigSetupCheckModule.php',
'PhabricatorConfigSiteModule' => 'applications/config/module/PhabricatorConfigSiteModule.php',
'PhabricatorConfigSiteSource' => 'infrastructure/env/PhabricatorConfigSiteSource.php',
'PhabricatorConfigSource' => 'infrastructure/env/PhabricatorConfigSource.php',
'PhabricatorConfigStackSource' => 'infrastructure/env/PhabricatorConfigStackSource.php',
'PhabricatorConfigStorageSchema' => 'applications/config/schema/PhabricatorConfigStorageSchema.php',
'PhabricatorConfigTableSchema' => 'applications/config/schema/PhabricatorConfigTableSchema.php',
'PhabricatorConfigTransaction' => 'applications/config/storage/PhabricatorConfigTransaction.php',
'PhabricatorConfigTransactionQuery' => 'applications/config/query/PhabricatorConfigTransactionQuery.php',
'PhabricatorConfigType' => 'applications/config/type/PhabricatorConfigType.php',
'PhabricatorConfigValidationException' => 'applications/config/exception/PhabricatorConfigValidationException.php',
'PhabricatorConfigVersionController' => 'applications/config/controller/PhabricatorConfigVersionController.php',
'PhabricatorConpherenceApplication' => 'applications/conpherence/application/PhabricatorConpherenceApplication.php',
'PhabricatorConpherenceColumnMinimizeSetting' => 'applications/settings/setting/PhabricatorConpherenceColumnMinimizeSetting.php',
'PhabricatorConpherenceColumnVisibleSetting' => 'applications/settings/setting/PhabricatorConpherenceColumnVisibleSetting.php',
'PhabricatorConpherenceNotificationsSetting' => 'applications/settings/setting/PhabricatorConpherenceNotificationsSetting.php',
'PhabricatorConpherencePreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php',
'PhabricatorConpherenceProfileMenuItem' => 'applications/search/menuitem/PhabricatorConpherenceProfileMenuItem.php',
'PhabricatorConpherenceRoomContextFreeGrammar' => 'applications/conpherence/lipsum/PhabricatorConpherenceRoomContextFreeGrammar.php',
'PhabricatorConpherenceRoomTestDataGenerator' => 'applications/conpherence/lipsum/PhabricatorConpherenceRoomTestDataGenerator.php',
'PhabricatorConpherenceSoundSetting' => 'applications/settings/setting/PhabricatorConpherenceSoundSetting.php',
'PhabricatorConpherenceThreadPHIDType' => 'applications/conpherence/phid/PhabricatorConpherenceThreadPHIDType.php',
'PhabricatorConpherenceWidgetVisibleSetting' => 'applications/settings/setting/PhabricatorConpherenceWidgetVisibleSetting.php',
'PhabricatorConsoleApplication' => 'applications/console/application/PhabricatorConsoleApplication.php',
'PhabricatorConsoleContentSource' => 'infrastructure/contentsource/PhabricatorConsoleContentSource.php',
'PhabricatorContactNumbersSettingsPanel' => 'applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php',
'PhabricatorContentSource' => 'infrastructure/contentsource/PhabricatorContentSource.php',
'PhabricatorContentSourceModule' => 'infrastructure/contentsource/PhabricatorContentSourceModule.php',
'PhabricatorContentSourceView' => 'infrastructure/contentsource/PhabricatorContentSourceView.php',
'PhabricatorContributedToObjectEdgeType' => 'applications/transactions/edges/PhabricatorContributedToObjectEdgeType.php',
'PhabricatorController' => 'applications/base/controller/PhabricatorController.php',
'PhabricatorCookies' => 'applications/auth/constants/PhabricatorCookies.php',
'PhabricatorCoreConfigOptions' => 'applications/config/option/PhabricatorCoreConfigOptions.php',
'PhabricatorCoreCreateTransaction' => 'applications/transactions/xaction/PhabricatorCoreCreateTransaction.php',
'PhabricatorCoreTransactionType' => 'applications/transactions/xaction/PhabricatorCoreTransactionType.php',
'PhabricatorCoreVoidTransaction' => 'applications/transactions/xaction/PhabricatorCoreVoidTransaction.php',
'PhabricatorCountFact' => 'applications/fact/fact/PhabricatorCountFact.php',
'PhabricatorCountdown' => 'applications/countdown/storage/PhabricatorCountdown.php',
'PhabricatorCountdownApplication' => 'applications/countdown/application/PhabricatorCountdownApplication.php',
'PhabricatorCountdownController' => 'applications/countdown/controller/PhabricatorCountdownController.php',
'PhabricatorCountdownCountdownPHIDType' => 'applications/countdown/phid/PhabricatorCountdownCountdownPHIDType.php',
'PhabricatorCountdownDAO' => 'applications/countdown/storage/PhabricatorCountdownDAO.php',
'PhabricatorCountdownDefaultEditCapability' => 'applications/countdown/capability/PhabricatorCountdownDefaultEditCapability.php',
'PhabricatorCountdownDefaultViewCapability' => 'applications/countdown/capability/PhabricatorCountdownDefaultViewCapability.php',
'PhabricatorCountdownDescriptionTransaction' => 'applications/countdown/xaction/PhabricatorCountdownDescriptionTransaction.php',
'PhabricatorCountdownEditController' => 'applications/countdown/controller/PhabricatorCountdownEditController.php',
'PhabricatorCountdownEditEngine' => 'applications/countdown/editor/PhabricatorCountdownEditEngine.php',
'PhabricatorCountdownEditor' => 'applications/countdown/editor/PhabricatorCountdownEditor.php',
'PhabricatorCountdownEpochTransaction' => 'applications/countdown/xaction/PhabricatorCountdownEpochTransaction.php',
'PhabricatorCountdownListController' => 'applications/countdown/controller/PhabricatorCountdownListController.php',
'PhabricatorCountdownMailReceiver' => 'applications/countdown/mail/PhabricatorCountdownMailReceiver.php',
'PhabricatorCountdownQuery' => 'applications/countdown/query/PhabricatorCountdownQuery.php',
'PhabricatorCountdownRemarkupRule' => 'applications/countdown/remarkup/PhabricatorCountdownRemarkupRule.php',
'PhabricatorCountdownReplyHandler' => 'applications/countdown/mail/PhabricatorCountdownReplyHandler.php',
'PhabricatorCountdownSchemaSpec' => 'applications/countdown/storage/PhabricatorCountdownSchemaSpec.php',
'PhabricatorCountdownSearchEngine' => 'applications/countdown/query/PhabricatorCountdownSearchEngine.php',
'PhabricatorCountdownTitleTransaction' => 'applications/countdown/xaction/PhabricatorCountdownTitleTransaction.php',
'PhabricatorCountdownTransaction' => 'applications/countdown/storage/PhabricatorCountdownTransaction.php',
'PhabricatorCountdownTransactionComment' => 'applications/countdown/storage/PhabricatorCountdownTransactionComment.php',
'PhabricatorCountdownTransactionQuery' => 'applications/countdown/query/PhabricatorCountdownTransactionQuery.php',
'PhabricatorCountdownTransactionType' => 'applications/countdown/xaction/PhabricatorCountdownTransactionType.php',
'PhabricatorCountdownView' => 'applications/countdown/view/PhabricatorCountdownView.php',
'PhabricatorCountdownViewController' => 'applications/countdown/controller/PhabricatorCountdownViewController.php',
'PhabricatorCredentialEditField' => 'applications/transactions/editfield/PhabricatorCredentialEditField.php',
'PhabricatorCursorPagedPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php',
'PhabricatorCustomField' => 'infrastructure/customfield/field/PhabricatorCustomField.php',
'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource.php',
'PhabricatorCustomFieldApplicationSearchDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchDatasource.php',
'PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource.php',
'PhabricatorCustomFieldAttachment' => 'infrastructure/customfield/field/PhabricatorCustomFieldAttachment.php',
'PhabricatorCustomFieldConfigOptionType' => 'infrastructure/customfield/config/PhabricatorCustomFieldConfigOptionType.php',
'PhabricatorCustomFieldDataNotAvailableException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldDataNotAvailableException.php',
'PhabricatorCustomFieldEditEngineExtension' => 'infrastructure/customfield/engineextension/PhabricatorCustomFieldEditEngineExtension.php',
'PhabricatorCustomFieldEditField' => 'infrastructure/customfield/editor/PhabricatorCustomFieldEditField.php',
'PhabricatorCustomFieldEditType' => 'infrastructure/customfield/editor/PhabricatorCustomFieldEditType.php',
'PhabricatorCustomFieldExportEngineExtension' => 'infrastructure/export/engine/PhabricatorCustomFieldExportEngineExtension.php',
'PhabricatorCustomFieldFulltextEngineExtension' => 'infrastructure/customfield/engineextension/PhabricatorCustomFieldFulltextEngineExtension.php',
'PhabricatorCustomFieldHeraldAction' => 'infrastructure/customfield/herald/PhabricatorCustomFieldHeraldAction.php',
'PhabricatorCustomFieldHeraldActionGroup' => 'infrastructure/customfield/herald/PhabricatorCustomFieldHeraldActionGroup.php',
'PhabricatorCustomFieldHeraldField' => 'infrastructure/customfield/herald/PhabricatorCustomFieldHeraldField.php',
'PhabricatorCustomFieldHeraldFieldGroup' => 'infrastructure/customfield/herald/PhabricatorCustomFieldHeraldFieldGroup.php',
'PhabricatorCustomFieldImplementationIncompleteException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldImplementationIncompleteException.php',
'PhabricatorCustomFieldIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php',
'PhabricatorCustomFieldInterface' => 'infrastructure/customfield/interface/PhabricatorCustomFieldInterface.php',
'PhabricatorCustomFieldList' => 'infrastructure/customfield/field/PhabricatorCustomFieldList.php',
'PhabricatorCustomFieldMonogramParser' => 'infrastructure/customfield/parser/PhabricatorCustomFieldMonogramParser.php',
'PhabricatorCustomFieldNotAttachedException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldNotAttachedException.php',
'PhabricatorCustomFieldNotProxyException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldNotProxyException.php',
'PhabricatorCustomFieldNumericIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldNumericIndexStorage.php',
'PhabricatorCustomFieldSearchEngineExtension' => 'infrastructure/customfield/engineextension/PhabricatorCustomFieldSearchEngineExtension.php',
'PhabricatorCustomFieldStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStorage.php',
'PhabricatorCustomFieldStorageQuery' => 'infrastructure/customfield/query/PhabricatorCustomFieldStorageQuery.php',
'PhabricatorCustomFieldStringIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php',
'PhabricatorCustomLogoConfigType' => 'applications/config/custom/PhabricatorCustomLogoConfigType.php',
'PhabricatorCustomUIFooterConfigType' => 'applications/config/custom/PhabricatorCustomUIFooterConfigType.php',
'PhabricatorDaemon' => 'infrastructure/daemon/PhabricatorDaemon.php',
'PhabricatorDaemonBulkJobController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobController.php',
'PhabricatorDaemonBulkJobListController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobListController.php',
'PhabricatorDaemonBulkJobMonitorController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobMonitorController.php',
'PhabricatorDaemonBulkJobViewController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php',
'PhabricatorDaemonConsoleController' => 'applications/daemon/controller/PhabricatorDaemonConsoleController.php',
'PhabricatorDaemonContentSource' => 'infrastructure/daemon/contentsource/PhabricatorDaemonContentSource.php',
'PhabricatorDaemonController' => 'applications/daemon/controller/PhabricatorDaemonController.php',
'PhabricatorDaemonDAO' => 'applications/daemon/storage/PhabricatorDaemonDAO.php',
'PhabricatorDaemonEventListener' => 'applications/daemon/event/PhabricatorDaemonEventListener.php',
'PhabricatorDaemonLockLog' => 'applications/daemon/storage/PhabricatorDaemonLockLog.php',
'PhabricatorDaemonLockLogGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonLockLogGarbageCollector.php',
'PhabricatorDaemonLog' => 'applications/daemon/storage/PhabricatorDaemonLog.php',
'PhabricatorDaemonLogEvent' => 'applications/daemon/storage/PhabricatorDaemonLogEvent.php',
'PhabricatorDaemonLogEventGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonLogEventGarbageCollector.php',
- 'PhabricatorDaemonLogEventViewController' => 'applications/daemon/controller/PhabricatorDaemonLogEventViewController.php',
- 'PhabricatorDaemonLogEventsView' => 'applications/daemon/view/PhabricatorDaemonLogEventsView.php',
'PhabricatorDaemonLogGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonLogGarbageCollector.php',
'PhabricatorDaemonLogListController' => 'applications/daemon/controller/PhabricatorDaemonLogListController.php',
'PhabricatorDaemonLogListView' => 'applications/daemon/view/PhabricatorDaemonLogListView.php',
'PhabricatorDaemonLogQuery' => 'applications/daemon/query/PhabricatorDaemonLogQuery.php',
'PhabricatorDaemonLogViewController' => 'applications/daemon/controller/PhabricatorDaemonLogViewController.php',
'PhabricatorDaemonManagementDebugWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementDebugWorkflow.php',
'PhabricatorDaemonManagementLaunchWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementLaunchWorkflow.php',
'PhabricatorDaemonManagementListWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementListWorkflow.php',
'PhabricatorDaemonManagementLogWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementLogWorkflow.php',
'PhabricatorDaemonManagementReloadWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementReloadWorkflow.php',
'PhabricatorDaemonManagementRestartWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php',
'PhabricatorDaemonManagementStartWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementStartWorkflow.php',
'PhabricatorDaemonManagementStatusWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php',
'PhabricatorDaemonManagementStopWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php',
'PhabricatorDaemonManagementWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementWorkflow.php',
'PhabricatorDaemonOverseerModule' => 'infrastructure/daemon/overseer/PhabricatorDaemonOverseerModule.php',
'PhabricatorDaemonReference' => 'infrastructure/daemon/control/PhabricatorDaemonReference.php',
'PhabricatorDaemonTaskGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonTaskGarbageCollector.php',
'PhabricatorDaemonTasksTableView' => 'applications/daemon/view/PhabricatorDaemonTasksTableView.php',
'PhabricatorDaemonsApplication' => 'applications/daemon/application/PhabricatorDaemonsApplication.php',
'PhabricatorDaemonsSetupCheck' => 'applications/config/check/PhabricatorDaemonsSetupCheck.php',
'PhabricatorDailyRoutineTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorDailyRoutineTriggerClock.php',
'PhabricatorDarkConsoleSetting' => 'applications/settings/setting/PhabricatorDarkConsoleSetting.php',
'PhabricatorDarkConsoleTabSetting' => 'applications/settings/setting/PhabricatorDarkConsoleTabSetting.php',
'PhabricatorDarkConsoleVisibleSetting' => 'applications/settings/setting/PhabricatorDarkConsoleVisibleSetting.php',
'PhabricatorDashboard' => 'applications/dashboard/storage/PhabricatorDashboard.php',
'PhabricatorDashboardAddPanelController' => 'applications/dashboard/controller/PhabricatorDashboardAddPanelController.php',
'PhabricatorDashboardApplication' => 'applications/dashboard/application/PhabricatorDashboardApplication.php',
'PhabricatorDashboardArchiveController' => 'applications/dashboard/controller/PhabricatorDashboardArchiveController.php',
'PhabricatorDashboardArrangeController' => 'applications/dashboard/controller/PhabricatorDashboardArrangeController.php',
'PhabricatorDashboardController' => 'applications/dashboard/controller/PhabricatorDashboardController.php',
'PhabricatorDashboardDAO' => 'applications/dashboard/storage/PhabricatorDashboardDAO.php',
'PhabricatorDashboardDashboardHasPanelEdgeType' => 'applications/dashboard/edge/PhabricatorDashboardDashboardHasPanelEdgeType.php',
'PhabricatorDashboardDashboardPHIDType' => 'applications/dashboard/phid/PhabricatorDashboardDashboardPHIDType.php',
'PhabricatorDashboardDatasource' => 'applications/dashboard/typeahead/PhabricatorDashboardDatasource.php',
'PhabricatorDashboardEditController' => 'applications/dashboard/controller/PhabricatorDashboardEditController.php',
'PhabricatorDashboardIconSet' => 'applications/dashboard/icon/PhabricatorDashboardIconSet.php',
'PhabricatorDashboardInstall' => 'applications/dashboard/storage/PhabricatorDashboardInstall.php',
'PhabricatorDashboardInstallController' => 'applications/dashboard/controller/PhabricatorDashboardInstallController.php',
'PhabricatorDashboardLayoutConfig' => 'applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php',
'PhabricatorDashboardListController' => 'applications/dashboard/controller/PhabricatorDashboardListController.php',
'PhabricatorDashboardManageController' => 'applications/dashboard/controller/PhabricatorDashboardManageController.php',
'PhabricatorDashboardMovePanelController' => 'applications/dashboard/controller/PhabricatorDashboardMovePanelController.php',
'PhabricatorDashboardNgrams' => 'applications/dashboard/storage/PhabricatorDashboardNgrams.php',
'PhabricatorDashboardPanel' => 'applications/dashboard/storage/PhabricatorDashboardPanel.php',
'PhabricatorDashboardPanelArchiveController' => 'applications/dashboard/controller/PhabricatorDashboardPanelArchiveController.php',
'PhabricatorDashboardPanelCoreCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelCoreCustomField.php',
'PhabricatorDashboardPanelCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelCustomField.php',
'PhabricatorDashboardPanelDatasource' => 'applications/dashboard/typeahead/PhabricatorDashboardPanelDatasource.php',
'PhabricatorDashboardPanelEditConduitAPIMethod' => 'applications/dashboard/conduit/PhabricatorDashboardPanelEditConduitAPIMethod.php',
'PhabricatorDashboardPanelEditController' => 'applications/dashboard/controller/PhabricatorDashboardPanelEditController.php',
'PhabricatorDashboardPanelEditEngine' => 'applications/dashboard/editor/PhabricatorDashboardPanelEditEngine.php',
'PhabricatorDashboardPanelEditproController' => 'applications/dashboard/controller/PhabricatorDashboardPanelEditproController.php',
'PhabricatorDashboardPanelHasDashboardEdgeType' => 'applications/dashboard/edge/PhabricatorDashboardPanelHasDashboardEdgeType.php',
'PhabricatorDashboardPanelListController' => 'applications/dashboard/controller/PhabricatorDashboardPanelListController.php',
'PhabricatorDashboardPanelNgrams' => 'applications/dashboard/storage/PhabricatorDashboardPanelNgrams.php',
'PhabricatorDashboardPanelPHIDType' => 'applications/dashboard/phid/PhabricatorDashboardPanelPHIDType.php',
'PhabricatorDashboardPanelQuery' => 'applications/dashboard/query/PhabricatorDashboardPanelQuery.php',
'PhabricatorDashboardPanelRenderController' => 'applications/dashboard/controller/PhabricatorDashboardPanelRenderController.php',
'PhabricatorDashboardPanelRenderingEngine' => 'applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php',
'PhabricatorDashboardPanelSearchApplicationCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelSearchApplicationCustomField.php',
'PhabricatorDashboardPanelSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardPanelSearchEngine.php',
'PhabricatorDashboardPanelSearchQueryCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelSearchQueryCustomField.php',
'PhabricatorDashboardPanelTabsCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelTabsCustomField.php',
'PhabricatorDashboardPanelTransaction' => 'applications/dashboard/storage/PhabricatorDashboardPanelTransaction.php',
'PhabricatorDashboardPanelTransactionEditor' => 'applications/dashboard/editor/PhabricatorDashboardPanelTransactionEditor.php',
'PhabricatorDashboardPanelTransactionQuery' => 'applications/dashboard/query/PhabricatorDashboardPanelTransactionQuery.php',
'PhabricatorDashboardPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardPanelType.php',
'PhabricatorDashboardPanelViewController' => 'applications/dashboard/controller/PhabricatorDashboardPanelViewController.php',
'PhabricatorDashboardProfileController' => 'applications/dashboard/controller/PhabricatorDashboardProfileController.php',
'PhabricatorDashboardProfileMenuItem' => 'applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php',
'PhabricatorDashboardQuery' => 'applications/dashboard/query/PhabricatorDashboardQuery.php',
'PhabricatorDashboardQueryPanelInstallController' => 'applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php',
'PhabricatorDashboardQueryPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php',
'PhabricatorDashboardRemarkupRule' => 'applications/dashboard/remarkup/PhabricatorDashboardRemarkupRule.php',
'PhabricatorDashboardRemovePanelController' => 'applications/dashboard/controller/PhabricatorDashboardRemovePanelController.php',
'PhabricatorDashboardRenderingEngine' => 'applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php',
'PhabricatorDashboardSchemaSpec' => 'applications/dashboard/storage/PhabricatorDashboardSchemaSpec.php',
'PhabricatorDashboardSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardSearchEngine.php',
'PhabricatorDashboardTabsPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardTabsPanelType.php',
'PhabricatorDashboardTextPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardTextPanelType.php',
'PhabricatorDashboardTransaction' => 'applications/dashboard/storage/PhabricatorDashboardTransaction.php',
'PhabricatorDashboardTransactionEditor' => 'applications/dashboard/editor/PhabricatorDashboardTransactionEditor.php',
'PhabricatorDashboardTransactionQuery' => 'applications/dashboard/query/PhabricatorDashboardTransactionQuery.php',
'PhabricatorDashboardViewController' => 'applications/dashboard/controller/PhabricatorDashboardViewController.php',
'PhabricatorDataCacheSpec' => 'applications/cache/spec/PhabricatorDataCacheSpec.php',
'PhabricatorDataNotAttachedException' => 'infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php',
'PhabricatorDatabaseRef' => 'infrastructure/cluster/PhabricatorDatabaseRef.php',
'PhabricatorDatabaseRefParser' => 'infrastructure/cluster/PhabricatorDatabaseRefParser.php',
'PhabricatorDatabaseSetupCheck' => 'applications/config/check/PhabricatorDatabaseSetupCheck.php',
'PhabricatorDatasourceApplicationEngineExtension' => 'applications/meta/engineextension/PhabricatorDatasourceApplicationEngineExtension.php',
'PhabricatorDatasourceEditField' => 'applications/transactions/editfield/PhabricatorDatasourceEditField.php',
'PhabricatorDatasourceEditType' => 'applications/transactions/edittype/PhabricatorDatasourceEditType.php',
'PhabricatorDatasourceEngine' => 'applications/search/engine/PhabricatorDatasourceEngine.php',
'PhabricatorDatasourceEngineExtension' => 'applications/search/engineextension/PhabricatorDatasourceEngineExtension.php',
'PhabricatorDateFormatSetting' => 'applications/settings/setting/PhabricatorDateFormatSetting.php',
'PhabricatorDateTimeSettingsPanel' => 'applications/settings/panel/PhabricatorDateTimeSettingsPanel.php',
'PhabricatorDebugController' => 'applications/system/controller/PhabricatorDebugController.php',
'PhabricatorDefaultRequestExceptionHandler' => 'aphront/handler/PhabricatorDefaultRequestExceptionHandler.php',
'PhabricatorDefaultSyntaxStyle' => 'infrastructure/syntax/PhabricatorDefaultSyntaxStyle.php',
+ 'PhabricatorDefaultUnlockEngine' => 'applications/system/engine/PhabricatorDefaultUnlockEngine.php',
'PhabricatorDestructibleCodex' => 'applications/system/codex/PhabricatorDestructibleCodex.php',
'PhabricatorDestructibleCodexInterface' => 'applications/system/interface/PhabricatorDestructibleCodexInterface.php',
'PhabricatorDestructibleInterface' => 'applications/system/interface/PhabricatorDestructibleInterface.php',
'PhabricatorDestructionEngine' => 'applications/system/engine/PhabricatorDestructionEngine.php',
'PhabricatorDestructionEngineExtension' => 'applications/system/engine/PhabricatorDestructionEngineExtension.php',
'PhabricatorDestructionEngineExtensionModule' => 'applications/system/engine/PhabricatorDestructionEngineExtensionModule.php',
'PhabricatorDeveloperConfigOptions' => 'applications/config/option/PhabricatorDeveloperConfigOptions.php',
'PhabricatorDeveloperPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php',
'PhabricatorDiffInlineCommentQuery' => 'infrastructure/diff/query/PhabricatorDiffInlineCommentQuery.php',
'PhabricatorDiffPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php',
+ 'PhabricatorDiffScopeEngine' => 'infrastructure/diff/PhabricatorDiffScopeEngine.php',
+ 'PhabricatorDiffScopeEngineTestCase' => 'infrastructure/diff/__tests__/PhabricatorDiffScopeEngineTestCase.php',
'PhabricatorDifferenceEngine' => 'infrastructure/diff/PhabricatorDifferenceEngine.php',
'PhabricatorDifferentialApplication' => 'applications/differential/application/PhabricatorDifferentialApplication.php',
'PhabricatorDifferentialAttachCommitWorkflow' => 'applications/differential/management/PhabricatorDifferentialAttachCommitWorkflow.php',
'PhabricatorDifferentialConfigOptions' => 'applications/differential/config/PhabricatorDifferentialConfigOptions.php',
'PhabricatorDifferentialExtractWorkflow' => 'applications/differential/management/PhabricatorDifferentialExtractWorkflow.php',
'PhabricatorDifferentialManagementWorkflow' => 'applications/differential/management/PhabricatorDifferentialManagementWorkflow.php',
'PhabricatorDifferentialMigrateHunkWorkflow' => 'applications/differential/management/PhabricatorDifferentialMigrateHunkWorkflow.php',
'PhabricatorDifferentialRebuildChangesetsWorkflow' => 'applications/differential/management/PhabricatorDifferentialRebuildChangesetsWorkflow.php',
'PhabricatorDifferentialRevisionTestDataGenerator' => 'applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php',
'PhabricatorDiffusionApplication' => 'applications/diffusion/application/PhabricatorDiffusionApplication.php',
'PhabricatorDiffusionBlameSetting' => 'applications/settings/setting/PhabricatorDiffusionBlameSetting.php',
'PhabricatorDiffusionConfigOptions' => 'applications/diffusion/config/PhabricatorDiffusionConfigOptions.php',
'PhabricatorDisabledUserController' => 'applications/auth/controller/PhabricatorDisabledUserController.php',
'PhabricatorDisplayPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDisplayPreferencesSettingsPanel.php',
'PhabricatorDisqusAuthProvider' => 'applications/auth/provider/PhabricatorDisqusAuthProvider.php',
'PhabricatorDividerEditField' => 'applications/transactions/editfield/PhabricatorDividerEditField.php',
'PhabricatorDividerProfileMenuItem' => 'applications/search/menuitem/PhabricatorDividerProfileMenuItem.php',
'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',
'PhabricatorDoubleExportField' => 'infrastructure/export/field/PhabricatorDoubleExportField.php',
'PhabricatorDraft' => 'applications/draft/storage/PhabricatorDraft.php',
'PhabricatorDraftDAO' => 'applications/draft/storage/PhabricatorDraftDAO.php',
'PhabricatorDraftEngine' => 'applications/transactions/draft/PhabricatorDraftEngine.php',
'PhabricatorDraftInterface' => 'applications/transactions/draft/PhabricatorDraftInterface.php',
'PhabricatorDrydockApplication' => 'applications/drydock/application/PhabricatorDrydockApplication.php',
'PhabricatorDuoAuthFactor' => 'applications/auth/factor/PhabricatorDuoAuthFactor.php',
'PhabricatorDuoFuture' => 'applications/auth/future/PhabricatorDuoFuture.php',
'PhabricatorEdgeChangeRecord' => 'infrastructure/edges/util/PhabricatorEdgeChangeRecord.php',
'PhabricatorEdgeChangeRecordTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeChangeRecordTestCase.php',
'PhabricatorEdgeConfig' => 'infrastructure/edges/constants/PhabricatorEdgeConfig.php',
'PhabricatorEdgeConstants' => 'infrastructure/edges/constants/PhabricatorEdgeConstants.php',
'PhabricatorEdgeCycleException' => 'infrastructure/edges/exception/PhabricatorEdgeCycleException.php',
'PhabricatorEdgeEditType' => 'applications/transactions/edittype/PhabricatorEdgeEditType.php',
'PhabricatorEdgeEditor' => 'infrastructure/edges/editor/PhabricatorEdgeEditor.php',
'PhabricatorEdgeGraph' => 'infrastructure/edges/util/PhabricatorEdgeGraph.php',
'PhabricatorEdgeObject' => 'infrastructure/edges/conduit/PhabricatorEdgeObject.php',
'PhabricatorEdgeObjectQuery' => 'infrastructure/edges/query/PhabricatorEdgeObjectQuery.php',
'PhabricatorEdgeQuery' => 'infrastructure/edges/query/PhabricatorEdgeQuery.php',
'PhabricatorEdgeTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php',
'PhabricatorEdgeType' => 'infrastructure/edges/type/PhabricatorEdgeType.php',
'PhabricatorEdgeTypeTestCase' => 'infrastructure/edges/type/__tests__/PhabricatorEdgeTypeTestCase.php',
'PhabricatorEdgesDestructionEngineExtension' => 'infrastructure/edges/engineextension/PhabricatorEdgesDestructionEngineExtension.php',
'PhabricatorEditEngine' => 'applications/transactions/editengine/PhabricatorEditEngine.php',
'PhabricatorEditEngineAPIMethod' => 'applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php',
'PhabricatorEditEngineBulkJobType' => 'applications/transactions/bulk/PhabricatorEditEngineBulkJobType.php',
'PhabricatorEditEngineCheckboxesCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineCheckboxesCommentAction.php',
'PhabricatorEditEngineColumnsCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineColumnsCommentAction.php',
'PhabricatorEditEngineCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineCommentAction.php',
'PhabricatorEditEngineCommentActionGroup' => 'applications/transactions/commentaction/PhabricatorEditEngineCommentActionGroup.php',
'PhabricatorEditEngineConfiguration' => 'applications/transactions/storage/PhabricatorEditEngineConfiguration.php',
'PhabricatorEditEngineConfigurationDefaultCreateController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultCreateController.php',
'PhabricatorEditEngineConfigurationDefaultsController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultsController.php',
'PhabricatorEditEngineConfigurationDisableController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationDisableController.php',
'PhabricatorEditEngineConfigurationEditController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationEditController.php',
'PhabricatorEditEngineConfigurationEditEngine' => 'applications/transactions/editor/PhabricatorEditEngineConfigurationEditEngine.php',
'PhabricatorEditEngineConfigurationEditor' => 'applications/transactions/editor/PhabricatorEditEngineConfigurationEditor.php',
'PhabricatorEditEngineConfigurationIsEditController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationIsEditController.php',
'PhabricatorEditEngineConfigurationListController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationListController.php',
'PhabricatorEditEngineConfigurationLockController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationLockController.php',
'PhabricatorEditEngineConfigurationPHIDType' => 'applications/transactions/phid/PhabricatorEditEngineConfigurationPHIDType.php',
'PhabricatorEditEngineConfigurationQuery' => 'applications/transactions/query/PhabricatorEditEngineConfigurationQuery.php',
'PhabricatorEditEngineConfigurationReorderController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationReorderController.php',
'PhabricatorEditEngineConfigurationSaveController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationSaveController.php',
'PhabricatorEditEngineConfigurationSearchEngine' => 'applications/transactions/query/PhabricatorEditEngineConfigurationSearchEngine.php',
'PhabricatorEditEngineConfigurationSortController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationSortController.php',
'PhabricatorEditEngineConfigurationSubtypeController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationSubtypeController.php',
'PhabricatorEditEngineConfigurationTransaction' => 'applications/transactions/storage/PhabricatorEditEngineConfigurationTransaction.php',
'PhabricatorEditEngineConfigurationTransactionQuery' => 'applications/transactions/query/PhabricatorEditEngineConfigurationTransactionQuery.php',
'PhabricatorEditEngineConfigurationViewController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationViewController.php',
'PhabricatorEditEngineController' => 'applications/transactions/controller/PhabricatorEditEngineController.php',
'PhabricatorEditEngineDatasource' => 'applications/transactions/typeahead/PhabricatorEditEngineDatasource.php',
'PhabricatorEditEngineDefaultLock' => 'applications/transactions/editengine/PhabricatorEditEngineDefaultLock.php',
'PhabricatorEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorEditEngineExtension.php',
'PhabricatorEditEngineExtensionModule' => 'applications/transactions/engineextension/PhabricatorEditEngineExtensionModule.php',
'PhabricatorEditEngineListController' => 'applications/transactions/controller/PhabricatorEditEngineListController.php',
'PhabricatorEditEngineLock' => 'applications/transactions/editengine/PhabricatorEditEngineLock.php',
'PhabricatorEditEngineLockableInterface' => 'applications/transactions/editengine/PhabricatorEditEngineLockableInterface.php',
'PhabricatorEditEngineMFAEngine' => 'applications/transactions/editengine/PhabricatorEditEngineMFAEngine.php',
'PhabricatorEditEngineMFAInterface' => 'applications/transactions/editengine/PhabricatorEditEngineMFAInterface.php',
'PhabricatorEditEnginePointsCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEnginePointsCommentAction.php',
'PhabricatorEditEngineProfileMenuItem' => 'applications/search/menuitem/PhabricatorEditEngineProfileMenuItem.php',
'PhabricatorEditEngineQuery' => 'applications/transactions/query/PhabricatorEditEngineQuery.php',
'PhabricatorEditEngineSearchEngine' => 'applications/transactions/query/PhabricatorEditEngineSearchEngine.php',
'PhabricatorEditEngineSelectCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineSelectCommentAction.php',
'PhabricatorEditEngineSettingsPanel' => 'applications/settings/panel/PhabricatorEditEngineSettingsPanel.php',
'PhabricatorEditEngineStaticCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineStaticCommentAction.php',
'PhabricatorEditEngineSubtype' => 'applications/transactions/editengine/PhabricatorEditEngineSubtype.php',
'PhabricatorEditEngineSubtypeInterface' => 'applications/transactions/editengine/PhabricatorEditEngineSubtypeInterface.php',
'PhabricatorEditEngineSubtypeMap' => 'applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php',
'PhabricatorEditEngineSubtypeTestCase' => 'applications/transactions/editengine/__tests__/PhabricatorEditEngineSubtypeTestCase.php',
'PhabricatorEditEngineTokenizerCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineTokenizerCommentAction.php',
'PhabricatorEditField' => 'applications/transactions/editfield/PhabricatorEditField.php',
'PhabricatorEditPage' => 'applications/transactions/editengine/PhabricatorEditPage.php',
'PhabricatorEditType' => 'applications/transactions/edittype/PhabricatorEditType.php',
'PhabricatorEditor' => 'infrastructure/PhabricatorEditor.php',
'PhabricatorEditorExtension' => 'applications/transactions/engineextension/PhabricatorEditorExtension.php',
'PhabricatorEditorExtensionModule' => 'applications/transactions/engineextension/PhabricatorEditorExtensionModule.php',
'PhabricatorEditorMailEngineExtension' => 'applications/transactions/engineextension/PhabricatorEditorMailEngineExtension.php',
'PhabricatorEditorMultipleSetting' => 'applications/settings/setting/PhabricatorEditorMultipleSetting.php',
'PhabricatorEditorSetting' => 'applications/settings/setting/PhabricatorEditorSetting.php',
'PhabricatorElasticFulltextStorageEngine' => 'applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php',
'PhabricatorElasticsearchHost' => 'infrastructure/cluster/search/PhabricatorElasticsearchHost.php',
'PhabricatorElasticsearchQueryBuilder' => 'applications/search/fulltextstorage/PhabricatorElasticsearchQueryBuilder.php',
'PhabricatorElasticsearchSetupCheck' => 'applications/config/check/PhabricatorElasticsearchSetupCheck.php',
'PhabricatorEmailAddressesSettingsPanel' => 'applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php',
'PhabricatorEmailContentSource' => 'applications/metamta/contentsource/PhabricatorEmailContentSource.php',
'PhabricatorEmailDeliverySettingsPanel' => 'applications/settings/panel/PhabricatorEmailDeliverySettingsPanel.php',
'PhabricatorEmailFormatSetting' => 'applications/settings/setting/PhabricatorEmailFormatSetting.php',
'PhabricatorEmailFormatSettingsPanel' => 'applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php',
'PhabricatorEmailLoginController' => 'applications/auth/controller/PhabricatorEmailLoginController.php',
'PhabricatorEmailNotificationsSetting' => 'applications/settings/setting/PhabricatorEmailNotificationsSetting.php',
'PhabricatorEmailPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php',
'PhabricatorEmailRePrefixSetting' => 'applications/settings/setting/PhabricatorEmailRePrefixSetting.php',
'PhabricatorEmailSelfActionsSetting' => 'applications/settings/setting/PhabricatorEmailSelfActionsSetting.php',
'PhabricatorEmailStampsSetting' => 'applications/settings/setting/PhabricatorEmailStampsSetting.php',
'PhabricatorEmailTagsSetting' => 'applications/settings/setting/PhabricatorEmailTagsSetting.php',
'PhabricatorEmailVarySubjectsSetting' => 'applications/settings/setting/PhabricatorEmailVarySubjectsSetting.php',
'PhabricatorEmailVerificationController' => 'applications/auth/controller/PhabricatorEmailVerificationController.php',
'PhabricatorEmbedFileRemarkupRule' => 'applications/files/markup/PhabricatorEmbedFileRemarkupRule.php',
'PhabricatorEmojiDatasource' => 'applications/macro/typeahead/PhabricatorEmojiDatasource.php',
'PhabricatorEmojiRemarkupRule' => 'applications/macro/markup/PhabricatorEmojiRemarkupRule.php',
'PhabricatorEmojiTranslation' => 'infrastructure/internationalization/translation/PhabricatorEmojiTranslation.php',
- 'PhabricatorEmptyQueryException' => 'infrastructure/query/PhabricatorEmptyQueryException.php',
+ 'PhabricatorEmptyQueryException' => 'infrastructure/query/exception/PhabricatorEmptyQueryException.php',
'PhabricatorEnumConfigType' => 'applications/config/type/PhabricatorEnumConfigType.php',
'PhabricatorEnv' => 'infrastructure/env/PhabricatorEnv.php',
'PhabricatorEnvTestCase' => 'infrastructure/env/__tests__/PhabricatorEnvTestCase.php',
'PhabricatorEpochEditField' => 'applications/transactions/editfield/PhabricatorEpochEditField.php',
'PhabricatorEpochExportField' => 'infrastructure/export/field/PhabricatorEpochExportField.php',
'PhabricatorEvent' => 'infrastructure/events/PhabricatorEvent.php',
'PhabricatorEventEngine' => 'infrastructure/events/PhabricatorEventEngine.php',
'PhabricatorEventListener' => 'infrastructure/events/PhabricatorEventListener.php',
'PhabricatorEventType' => 'infrastructure/events/constant/PhabricatorEventType.php',
'PhabricatorExampleEventListener' => 'infrastructure/events/PhabricatorExampleEventListener.php',
'PhabricatorExcelExportFormat' => 'infrastructure/export/format/PhabricatorExcelExportFormat.php',
'PhabricatorExecFutureFileUploadSource' => 'applications/files/uploadsource/PhabricatorExecFutureFileUploadSource.php',
'PhabricatorExportEngine' => 'infrastructure/export/engine/PhabricatorExportEngine.php',
'PhabricatorExportEngineBulkJobType' => 'infrastructure/export/engine/PhabricatorExportEngineBulkJobType.php',
'PhabricatorExportEngineExtension' => 'infrastructure/export/engine/PhabricatorExportEngineExtension.php',
'PhabricatorExportField' => 'infrastructure/export/field/PhabricatorExportField.php',
'PhabricatorExportFormat' => 'infrastructure/export/format/PhabricatorExportFormat.php',
'PhabricatorExportFormatSetting' => 'infrastructure/export/engine/PhabricatorExportFormatSetting.php',
'PhabricatorExtendedPolicyInterface' => 'applications/policy/interface/PhabricatorExtendedPolicyInterface.php',
'PhabricatorExtendingPhabricatorConfigOptions' => 'applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php',
'PhabricatorExtensionsSetupCheck' => 'applications/config/check/PhabricatorExtensionsSetupCheck.php',
'PhabricatorExternalAccount' => 'applications/people/storage/PhabricatorExternalAccount.php',
'PhabricatorExternalAccountQuery' => 'applications/auth/query/PhabricatorExternalAccountQuery.php',
'PhabricatorExternalAccountsSettingsPanel' => 'applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php',
'PhabricatorExtraConfigSetupCheck' => 'applications/config/check/PhabricatorExtraConfigSetupCheck.php',
'PhabricatorFacebookAuthProvider' => 'applications/auth/provider/PhabricatorFacebookAuthProvider.php',
'PhabricatorFact' => 'applications/fact/fact/PhabricatorFact.php',
'PhabricatorFactAggregate' => 'applications/fact/storage/PhabricatorFactAggregate.php',
'PhabricatorFactApplication' => 'applications/fact/application/PhabricatorFactApplication.php',
'PhabricatorFactChartController' => 'applications/fact/controller/PhabricatorFactChartController.php',
'PhabricatorFactController' => 'applications/fact/controller/PhabricatorFactController.php',
'PhabricatorFactCursor' => 'applications/fact/storage/PhabricatorFactCursor.php',
'PhabricatorFactDAO' => 'applications/fact/storage/PhabricatorFactDAO.php',
'PhabricatorFactDaemon' => 'applications/fact/daemon/PhabricatorFactDaemon.php',
'PhabricatorFactDatapointQuery' => 'applications/fact/query/PhabricatorFactDatapointQuery.php',
'PhabricatorFactDimension' => 'applications/fact/storage/PhabricatorFactDimension.php',
'PhabricatorFactEngine' => 'applications/fact/engine/PhabricatorFactEngine.php',
'PhabricatorFactEngineTestCase' => 'applications/fact/engine/__tests__/PhabricatorFactEngineTestCase.php',
'PhabricatorFactHomeController' => 'applications/fact/controller/PhabricatorFactHomeController.php',
'PhabricatorFactIntDatapoint' => 'applications/fact/storage/PhabricatorFactIntDatapoint.php',
'PhabricatorFactKeyDimension' => 'applications/fact/storage/PhabricatorFactKeyDimension.php',
'PhabricatorFactManagementAnalyzeWorkflow' => 'applications/fact/management/PhabricatorFactManagementAnalyzeWorkflow.php',
'PhabricatorFactManagementCursorsWorkflow' => 'applications/fact/management/PhabricatorFactManagementCursorsWorkflow.php',
'PhabricatorFactManagementDestroyWorkflow' => 'applications/fact/management/PhabricatorFactManagementDestroyWorkflow.php',
'PhabricatorFactManagementListWorkflow' => 'applications/fact/management/PhabricatorFactManagementListWorkflow.php',
'PhabricatorFactManagementWorkflow' => 'applications/fact/management/PhabricatorFactManagementWorkflow.php',
'PhabricatorFactObjectController' => 'applications/fact/controller/PhabricatorFactObjectController.php',
'PhabricatorFactObjectDimension' => 'applications/fact/storage/PhabricatorFactObjectDimension.php',
'PhabricatorFactRaw' => 'applications/fact/storage/PhabricatorFactRaw.php',
'PhabricatorFactUpdateIterator' => 'applications/fact/extract/PhabricatorFactUpdateIterator.php',
'PhabricatorFaviconRef' => 'applications/files/favicon/PhabricatorFaviconRef.php',
'PhabricatorFaviconRefQuery' => 'applications/files/favicon/PhabricatorFaviconRefQuery.php',
'PhabricatorFavoritesApplication' => 'applications/favorites/application/PhabricatorFavoritesApplication.php',
'PhabricatorFavoritesController' => 'applications/favorites/controller/PhabricatorFavoritesController.php',
'PhabricatorFavoritesMainMenuBarExtension' => 'applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php',
'PhabricatorFavoritesMenuItemController' => 'applications/favorites/controller/PhabricatorFavoritesMenuItemController.php',
'PhabricatorFavoritesProfileMenuEngine' => 'applications/favorites/engine/PhabricatorFavoritesProfileMenuEngine.php',
'PhabricatorFaxContentSource' => 'infrastructure/contentsource/PhabricatorFaxContentSource.php',
'PhabricatorFeedApplication' => 'applications/feed/application/PhabricatorFeedApplication.php',
'PhabricatorFeedBuilder' => 'applications/feed/builder/PhabricatorFeedBuilder.php',
'PhabricatorFeedConfigOptions' => 'applications/feed/config/PhabricatorFeedConfigOptions.php',
'PhabricatorFeedController' => 'applications/feed/controller/PhabricatorFeedController.php',
'PhabricatorFeedDAO' => 'applications/feed/storage/PhabricatorFeedDAO.php',
'PhabricatorFeedDetailController' => 'applications/feed/controller/PhabricatorFeedDetailController.php',
'PhabricatorFeedListController' => 'applications/feed/controller/PhabricatorFeedListController.php',
'PhabricatorFeedManagementRepublishWorkflow' => 'applications/feed/management/PhabricatorFeedManagementRepublishWorkflow.php',
'PhabricatorFeedManagementWorkflow' => 'applications/feed/management/PhabricatorFeedManagementWorkflow.php',
'PhabricatorFeedQuery' => 'applications/feed/query/PhabricatorFeedQuery.php',
'PhabricatorFeedSearchEngine' => 'applications/feed/query/PhabricatorFeedSearchEngine.php',
'PhabricatorFeedStory' => 'applications/feed/story/PhabricatorFeedStory.php',
'PhabricatorFeedStoryData' => 'applications/feed/storage/PhabricatorFeedStoryData.php',
'PhabricatorFeedStoryNotification' => 'applications/notification/storage/PhabricatorFeedStoryNotification.php',
'PhabricatorFeedStoryPublisher' => 'applications/feed/PhabricatorFeedStoryPublisher.php',
'PhabricatorFeedStoryReference' => 'applications/feed/storage/PhabricatorFeedStoryReference.php',
'PhabricatorFerretEngine' => 'applications/search/ferret/PhabricatorFerretEngine.php',
'PhabricatorFerretEngineTestCase' => 'applications/search/ferret/__tests__/PhabricatorFerretEngineTestCase.php',
'PhabricatorFerretFulltextEngineExtension' => 'applications/search/engineextension/PhabricatorFerretFulltextEngineExtension.php',
'PhabricatorFerretFulltextStorageEngine' => 'applications/search/fulltextstorage/PhabricatorFerretFulltextStorageEngine.php',
'PhabricatorFerretInterface' => 'applications/search/ferret/PhabricatorFerretInterface.php',
'PhabricatorFerretMetadata' => 'applications/search/ferret/PhabricatorFerretMetadata.php',
'PhabricatorFerretSearchEngineExtension' => 'applications/search/engineextension/PhabricatorFerretSearchEngineExtension.php',
'PhabricatorFile' => 'applications/files/storage/PhabricatorFile.php',
'PhabricatorFileAES256StorageFormat' => 'applications/files/format/PhabricatorFileAES256StorageFormat.php',
'PhabricatorFileBundleLoader' => 'applications/files/query/PhabricatorFileBundleLoader.php',
'PhabricatorFileChunk' => 'applications/files/storage/PhabricatorFileChunk.php',
'PhabricatorFileChunkIterator' => 'applications/files/engine/PhabricatorFileChunkIterator.php',
'PhabricatorFileChunkQuery' => 'applications/files/query/PhabricatorFileChunkQuery.php',
'PhabricatorFileComposeController' => 'applications/files/controller/PhabricatorFileComposeController.php',
'PhabricatorFileController' => 'applications/files/controller/PhabricatorFileController.php',
'PhabricatorFileDAO' => 'applications/files/storage/PhabricatorFileDAO.php',
'PhabricatorFileDataController' => 'applications/files/controller/PhabricatorFileDataController.php',
'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',
'PhabricatorFileEditField' => 'applications/transactions/editfield/PhabricatorFileEditField.php',
'PhabricatorFileEditor' => 'applications/files/editor/PhabricatorFileEditor.php',
'PhabricatorFileExternalRequest' => 'applications/files/storage/PhabricatorFileExternalRequest.php',
'PhabricatorFileExternalRequestGarbageCollector' => 'applications/files/garbagecollector/PhabricatorFileExternalRequestGarbageCollector.php',
'PhabricatorFileFilePHIDType' => 'applications/files/phid/PhabricatorFileFilePHIDType.php',
'PhabricatorFileHasObjectEdgeType' => 'applications/files/edge/PhabricatorFileHasObjectEdgeType.php',
'PhabricatorFileIconSetSelectController' => 'applications/files/controller/PhabricatorFileIconSetSelectController.php',
'PhabricatorFileImageMacro' => 'applications/macro/storage/PhabricatorFileImageMacro.php',
'PhabricatorFileImageProxyController' => 'applications/files/controller/PhabricatorFileImageProxyController.php',
'PhabricatorFileImageTransform' => 'applications/files/transform/PhabricatorFileImageTransform.php',
'PhabricatorFileIntegrityException' => 'applications/files/exception/PhabricatorFileIntegrityException.php',
'PhabricatorFileLightboxController' => 'applications/files/controller/PhabricatorFileLightboxController.php',
'PhabricatorFileLinkView' => 'view/layout/PhabricatorFileLinkView.php',
'PhabricatorFileListController' => 'applications/files/controller/PhabricatorFileListController.php',
'PhabricatorFileNameNgrams' => 'applications/files/storage/PhabricatorFileNameNgrams.php',
'PhabricatorFileNameTransaction' => 'applications/files/xaction/PhabricatorFileNameTransaction.php',
'PhabricatorFileQuery' => 'applications/files/query/PhabricatorFileQuery.php',
'PhabricatorFileROT13StorageFormat' => 'applications/files/format/PhabricatorFileROT13StorageFormat.php',
'PhabricatorFileRawStorageFormat' => 'applications/files/format/PhabricatorFileRawStorageFormat.php',
'PhabricatorFileSchemaSpec' => 'applications/files/storage/PhabricatorFileSchemaSpec.php',
'PhabricatorFileSearchConduitAPIMethod' => 'applications/files/conduit/PhabricatorFileSearchConduitAPIMethod.php',
'PhabricatorFileSearchEngine' => 'applications/files/query/PhabricatorFileSearchEngine.php',
'PhabricatorFileStorageBlob' => 'applications/files/storage/PhabricatorFileStorageBlob.php',
'PhabricatorFileStorageConfigurationException' => 'applications/files/exception/PhabricatorFileStorageConfigurationException.php',
'PhabricatorFileStorageEngine' => 'applications/files/engine/PhabricatorFileStorageEngine.php',
'PhabricatorFileStorageEngineTestCase' => 'applications/files/engine/__tests__/PhabricatorFileStorageEngineTestCase.php',
'PhabricatorFileStorageFormat' => 'applications/files/format/PhabricatorFileStorageFormat.php',
'PhabricatorFileStorageFormatTestCase' => 'applications/files/format/__tests__/PhabricatorFileStorageFormatTestCase.php',
'PhabricatorFileTemporaryGarbageCollector' => 'applications/files/garbagecollector/PhabricatorFileTemporaryGarbageCollector.php',
'PhabricatorFileTestCase' => 'applications/files/storage/__tests__/PhabricatorFileTestCase.php',
'PhabricatorFileTestDataGenerator' => 'applications/files/lipsum/PhabricatorFileTestDataGenerator.php',
'PhabricatorFileThumbnailTransform' => 'applications/files/transform/PhabricatorFileThumbnailTransform.php',
'PhabricatorFileTransaction' => 'applications/files/storage/PhabricatorFileTransaction.php',
'PhabricatorFileTransactionComment' => 'applications/files/storage/PhabricatorFileTransactionComment.php',
'PhabricatorFileTransactionQuery' => 'applications/files/query/PhabricatorFileTransactionQuery.php',
'PhabricatorFileTransactionType' => 'applications/files/xaction/PhabricatorFileTransactionType.php',
'PhabricatorFileTransform' => 'applications/files/transform/PhabricatorFileTransform.php',
'PhabricatorFileTransformController' => 'applications/files/controller/PhabricatorFileTransformController.php',
'PhabricatorFileTransformListController' => 'applications/files/controller/PhabricatorFileTransformListController.php',
'PhabricatorFileTransformTestCase' => 'applications/files/transform/__tests__/PhabricatorFileTransformTestCase.php',
'PhabricatorFileUploadController' => 'applications/files/controller/PhabricatorFileUploadController.php',
'PhabricatorFileUploadDialogController' => 'applications/files/controller/PhabricatorFileUploadDialogController.php',
'PhabricatorFileUploadException' => 'applications/files/exception/PhabricatorFileUploadException.php',
'PhabricatorFileUploadSource' => 'applications/files/uploadsource/PhabricatorFileUploadSource.php',
'PhabricatorFileUploadSourceByteLimitException' => 'applications/files/uploadsource/PhabricatorFileUploadSourceByteLimitException.php',
'PhabricatorFileViewController' => 'applications/files/controller/PhabricatorFileViewController.php',
'PhabricatorFileinfoSetupCheck' => 'applications/config/check/PhabricatorFileinfoSetupCheck.php',
'PhabricatorFilesApplication' => 'applications/files/application/PhabricatorFilesApplication.php',
'PhabricatorFilesApplicationStorageEnginePanel' => 'applications/files/applicationpanel/PhabricatorFilesApplicationStorageEnginePanel.php',
'PhabricatorFilesBuiltinFile' => 'applications/files/builtin/PhabricatorFilesBuiltinFile.php',
'PhabricatorFilesComposeAvatarBuiltinFile' => 'applications/files/builtin/PhabricatorFilesComposeAvatarBuiltinFile.php',
'PhabricatorFilesComposeIconBuiltinFile' => 'applications/files/builtin/PhabricatorFilesComposeIconBuiltinFile.php',
'PhabricatorFilesConfigOptions' => 'applications/files/config/PhabricatorFilesConfigOptions.php',
'PhabricatorFilesManagementCatWorkflow' => 'applications/files/management/PhabricatorFilesManagementCatWorkflow.php',
'PhabricatorFilesManagementCompactWorkflow' => 'applications/files/management/PhabricatorFilesManagementCompactWorkflow.php',
'PhabricatorFilesManagementCycleWorkflow' => 'applications/files/management/PhabricatorFilesManagementCycleWorkflow.php',
'PhabricatorFilesManagementEncodeWorkflow' => 'applications/files/management/PhabricatorFilesManagementEncodeWorkflow.php',
'PhabricatorFilesManagementEnginesWorkflow' => 'applications/files/management/PhabricatorFilesManagementEnginesWorkflow.php',
'PhabricatorFilesManagementGenerateKeyWorkflow' => 'applications/files/management/PhabricatorFilesManagementGenerateKeyWorkflow.php',
'PhabricatorFilesManagementIntegrityWorkflow' => 'applications/files/management/PhabricatorFilesManagementIntegrityWorkflow.php',
'PhabricatorFilesManagementMigrateWorkflow' => 'applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php',
'PhabricatorFilesManagementRebuildWorkflow' => 'applications/files/management/PhabricatorFilesManagementRebuildWorkflow.php',
'PhabricatorFilesManagementWorkflow' => 'applications/files/management/PhabricatorFilesManagementWorkflow.php',
'PhabricatorFilesOnDiskBuiltinFile' => 'applications/files/builtin/PhabricatorFilesOnDiskBuiltinFile.php',
'PhabricatorFilesOutboundRequestAction' => 'applications/files/action/PhabricatorFilesOutboundRequestAction.php',
'PhabricatorFiletreeVisibleSetting' => 'applications/settings/setting/PhabricatorFiletreeVisibleSetting.php',
'PhabricatorFiletreeWidthSetting' => 'applications/settings/setting/PhabricatorFiletreeWidthSetting.php',
'PhabricatorFlag' => 'applications/flag/storage/PhabricatorFlag.php',
'PhabricatorFlagAddFlagHeraldAction' => 'applications/flag/herald/PhabricatorFlagAddFlagHeraldAction.php',
'PhabricatorFlagColor' => 'applications/flag/constants/PhabricatorFlagColor.php',
'PhabricatorFlagConstants' => 'applications/flag/constants/PhabricatorFlagConstants.php',
'PhabricatorFlagController' => 'applications/flag/controller/PhabricatorFlagController.php',
'PhabricatorFlagDAO' => 'applications/flag/storage/PhabricatorFlagDAO.php',
'PhabricatorFlagDeleteController' => 'applications/flag/controller/PhabricatorFlagDeleteController.php',
'PhabricatorFlagDestructionEngineExtension' => 'applications/flag/engineextension/PhabricatorFlagDestructionEngineExtension.php',
'PhabricatorFlagEditController' => 'applications/flag/controller/PhabricatorFlagEditController.php',
'PhabricatorFlagListController' => 'applications/flag/controller/PhabricatorFlagListController.php',
'PhabricatorFlagQuery' => 'applications/flag/query/PhabricatorFlagQuery.php',
'PhabricatorFlagSearchEngine' => 'applications/flag/query/PhabricatorFlagSearchEngine.php',
'PhabricatorFlagSelectControl' => 'applications/flag/view/PhabricatorFlagSelectControl.php',
'PhabricatorFlaggableInterface' => 'applications/flag/interface/PhabricatorFlaggableInterface.php',
'PhabricatorFlagsApplication' => 'applications/flag/application/PhabricatorFlagsApplication.php',
'PhabricatorFlagsUIEventListener' => 'applications/flag/events/PhabricatorFlagsUIEventListener.php',
'PhabricatorFulltextEngine' => 'applications/search/index/PhabricatorFulltextEngine.php',
'PhabricatorFulltextEngineExtension' => 'applications/search/index/PhabricatorFulltextEngineExtension.php',
'PhabricatorFulltextEngineExtensionModule' => 'applications/search/index/PhabricatorFulltextEngineExtensionModule.php',
'PhabricatorFulltextIndexEngineExtension' => 'applications/search/engineextension/PhabricatorFulltextIndexEngineExtension.php',
'PhabricatorFulltextInterface' => 'applications/search/interface/PhabricatorFulltextInterface.php',
'PhabricatorFulltextResultSet' => 'applications/search/query/PhabricatorFulltextResultSet.php',
'PhabricatorFulltextStorageEngine' => 'applications/search/fulltextstorage/PhabricatorFulltextStorageEngine.php',
'PhabricatorFulltextToken' => 'applications/search/query/PhabricatorFulltextToken.php',
'PhabricatorFundApplication' => 'applications/fund/application/PhabricatorFundApplication.php',
'PhabricatorGDSetupCheck' => 'applications/config/check/PhabricatorGDSetupCheck.php',
'PhabricatorGarbageCollector' => 'infrastructure/daemon/garbagecollector/PhabricatorGarbageCollector.php',
'PhabricatorGarbageCollectorManagementCollectWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementCollectWorkflow.php',
'PhabricatorGarbageCollectorManagementCompactEdgesWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementCompactEdgesWorkflow.php',
'PhabricatorGarbageCollectorManagementSetPolicyWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementSetPolicyWorkflow.php',
'PhabricatorGarbageCollectorManagementWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementWorkflow.php',
'PhabricatorGeneralCachePurger' => 'applications/cache/purger/PhabricatorGeneralCachePurger.php',
'PhabricatorGestureUIExample' => 'applications/uiexample/examples/PhabricatorGestureUIExample.php',
'PhabricatorGitGraphStream' => 'applications/repository/daemon/PhabricatorGitGraphStream.php',
'PhabricatorGitHubAuthProvider' => 'applications/auth/provider/PhabricatorGitHubAuthProvider.php',
'PhabricatorGlobalLock' => 'infrastructure/util/PhabricatorGlobalLock.php',
'PhabricatorGlobalUploadTargetView' => 'applications/files/view/PhabricatorGlobalUploadTargetView.php',
'PhabricatorGoogleAuthProvider' => 'applications/auth/provider/PhabricatorGoogleAuthProvider.php',
'PhabricatorGuidanceContext' => 'applications/guides/guidance/PhabricatorGuidanceContext.php',
'PhabricatorGuidanceEngine' => 'applications/guides/guidance/PhabricatorGuidanceEngine.php',
'PhabricatorGuidanceEngineExtension' => 'applications/guides/guidance/PhabricatorGuidanceEngineExtension.php',
'PhabricatorGuidanceMessage' => 'applications/guides/guidance/PhabricatorGuidanceMessage.php',
'PhabricatorGuideApplication' => 'applications/guides/application/PhabricatorGuideApplication.php',
'PhabricatorGuideController' => 'applications/guides/controller/PhabricatorGuideController.php',
'PhabricatorGuideInstallModule' => 'applications/guides/module/PhabricatorGuideInstallModule.php',
'PhabricatorGuideItemView' => 'applications/guides/view/PhabricatorGuideItemView.php',
'PhabricatorGuideListView' => 'applications/guides/view/PhabricatorGuideListView.php',
'PhabricatorGuideModule' => 'applications/guides/module/PhabricatorGuideModule.php',
'PhabricatorGuideModuleController' => 'applications/guides/controller/PhabricatorGuideModuleController.php',
'PhabricatorGuideQuickStartModule' => 'applications/guides/module/PhabricatorGuideQuickStartModule.php',
'PhabricatorHMACTestCase' => 'infrastructure/util/__tests__/PhabricatorHMACTestCase.php',
'PhabricatorHTTPParameterTypeTableView' => 'applications/config/view/PhabricatorHTTPParameterTypeTableView.php',
'PhabricatorHandleList' => 'applications/phid/handle/pool/PhabricatorHandleList.php',
'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php',
'PhabricatorHandlePool' => 'applications/phid/handle/pool/PhabricatorHandlePool.php',
'PhabricatorHandlePoolTestCase' => 'applications/phid/handle/pool/__tests__/PhabricatorHandlePoolTestCase.php',
'PhabricatorHandleQuery' => 'applications/phid/query/PhabricatorHandleQuery.php',
'PhabricatorHandleRemarkupRule' => 'applications/phid/remarkup/PhabricatorHandleRemarkupRule.php',
'PhabricatorHandlesEditField' => 'applications/transactions/editfield/PhabricatorHandlesEditField.php',
'PhabricatorHarbormasterApplication' => 'applications/harbormaster/application/PhabricatorHarbormasterApplication.php',
'PhabricatorHash' => 'infrastructure/util/PhabricatorHash.php',
'PhabricatorHashTestCase' => 'infrastructure/util/__tests__/PhabricatorHashTestCase.php',
'PhabricatorHelpApplication' => 'applications/help/application/PhabricatorHelpApplication.php',
'PhabricatorHelpController' => 'applications/help/controller/PhabricatorHelpController.php',
'PhabricatorHelpDocumentationController' => 'applications/help/controller/PhabricatorHelpDocumentationController.php',
'PhabricatorHelpEditorProtocolController' => 'applications/help/controller/PhabricatorHelpEditorProtocolController.php',
'PhabricatorHelpKeyboardShortcutController' => 'applications/help/controller/PhabricatorHelpKeyboardShortcutController.php',
'PhabricatorHeraldApplication' => 'applications/herald/application/PhabricatorHeraldApplication.php',
'PhabricatorHeraldContentSource' => 'applications/herald/contentsource/PhabricatorHeraldContentSource.php',
'PhabricatorHexdumpDocumentEngine' => 'applications/files/document/PhabricatorHexdumpDocumentEngine.php',
'PhabricatorHighSecurityRequestExceptionHandler' => 'aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php',
'PhabricatorHomeApplication' => 'applications/home/application/PhabricatorHomeApplication.php',
'PhabricatorHomeConstants' => 'applications/home/constants/PhabricatorHomeConstants.php',
'PhabricatorHomeController' => 'applications/home/controller/PhabricatorHomeController.php',
'PhabricatorHomeLauncherProfileMenuItem' => 'applications/home/menuitem/PhabricatorHomeLauncherProfileMenuItem.php',
'PhabricatorHomeMenuItemController' => 'applications/home/controller/PhabricatorHomeMenuItemController.php',
'PhabricatorHomeProfileMenuEngine' => 'applications/home/engine/PhabricatorHomeProfileMenuEngine.php',
'PhabricatorHomeProfileMenuItem' => 'applications/home/menuitem/PhabricatorHomeProfileMenuItem.php',
'PhabricatorHovercardEngineExtension' => 'applications/search/engineextension/PhabricatorHovercardEngineExtension.php',
'PhabricatorHovercardEngineExtensionModule' => 'applications/search/engineextension/PhabricatorHovercardEngineExtensionModule.php',
'PhabricatorIDExportField' => 'infrastructure/export/field/PhabricatorIDExportField.php',
'PhabricatorIDsSearchEngineExtension' => 'applications/search/engineextension/PhabricatorIDsSearchEngineExtension.php',
'PhabricatorIDsSearchField' => 'applications/search/field/PhabricatorIDsSearchField.php',
'PhabricatorIconDatasource' => 'applications/files/typeahead/PhabricatorIconDatasource.php',
'PhabricatorIconRemarkupRule' => 'applications/macro/markup/PhabricatorIconRemarkupRule.php',
'PhabricatorIconSet' => 'applications/files/iconset/PhabricatorIconSet.php',
'PhabricatorIconSetEditField' => 'applications/transactions/editfield/PhabricatorIconSetEditField.php',
'PhabricatorIconSetIcon' => 'applications/files/iconset/PhabricatorIconSetIcon.php',
'PhabricatorImageDocumentEngine' => 'applications/files/document/PhabricatorImageDocumentEngine.php',
'PhabricatorImageMacroRemarkupRule' => 'applications/macro/markup/PhabricatorImageMacroRemarkupRule.php',
'PhabricatorImageRemarkupRule' => 'applications/files/markup/PhabricatorImageRemarkupRule.php',
'PhabricatorImageTransformer' => 'applications/files/PhabricatorImageTransformer.php',
'PhabricatorImagemagickSetupCheck' => 'applications/config/check/PhabricatorImagemagickSetupCheck.php',
'PhabricatorInFlightErrorView' => 'applications/config/view/PhabricatorInFlightErrorView.php',
'PhabricatorIndexEngine' => 'applications/search/index/PhabricatorIndexEngine.php',
'PhabricatorIndexEngineExtension' => 'applications/search/index/PhabricatorIndexEngineExtension.php',
'PhabricatorIndexEngineExtensionModule' => 'applications/search/index/PhabricatorIndexEngineExtensionModule.php',
'PhabricatorIndexableInterface' => 'applications/search/interface/PhabricatorIndexableInterface.php',
'PhabricatorInfrastructureTestCase' => '__tests__/PhabricatorInfrastructureTestCase.php',
'PhabricatorInlineCommentController' => 'infrastructure/diff/PhabricatorInlineCommentController.php',
'PhabricatorInlineCommentInterface' => 'infrastructure/diff/interface/PhabricatorInlineCommentInterface.php',
'PhabricatorInlineCommentPreviewController' => 'infrastructure/diff/PhabricatorInlineCommentPreviewController.php',
'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',
'PhabricatorInternationalizationManagementWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementWorkflow.php',
'PhabricatorInvalidConfigSetupCheck' => 'applications/config/check/PhabricatorInvalidConfigSetupCheck.php',
+ 'PhabricatorInvalidQueryCursorException' => 'infrastructure/query/exception/PhabricatorInvalidQueryCursorException.php',
'PhabricatorIteratedMD5PasswordHasher' => 'infrastructure/util/password/PhabricatorIteratedMD5PasswordHasher.php',
'PhabricatorIteratedMD5PasswordHasherTestCase' => 'infrastructure/util/password/__tests__/PhabricatorIteratedMD5PasswordHasherTestCase.php',
'PhabricatorIteratorFileUploadSource' => 'applications/files/uploadsource/PhabricatorIteratorFileUploadSource.php',
'PhabricatorJIRAAuthProvider' => 'applications/auth/provider/PhabricatorJIRAAuthProvider.php',
'PhabricatorJSONConfigType' => 'applications/config/type/PhabricatorJSONConfigType.php',
'PhabricatorJSONDocumentEngine' => 'applications/files/document/PhabricatorJSONDocumentEngine.php',
'PhabricatorJSONExportFormat' => 'infrastructure/export/format/PhabricatorJSONExportFormat.php',
'PhabricatorJavelinLinter' => 'infrastructure/lint/linter/PhabricatorJavelinLinter.php',
'PhabricatorJiraIssueHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorJiraIssueHasObjectEdgeType.php',
'PhabricatorJupyterDocumentEngine' => 'applications/files/document/PhabricatorJupyterDocumentEngine.php',
'PhabricatorKeyValueDatabaseCache' => 'applications/cache/PhabricatorKeyValueDatabaseCache.php',
'PhabricatorKeyValueSerializingCacheProxy' => 'applications/cache/PhabricatorKeyValueSerializingCacheProxy.php',
'PhabricatorKeyboardRemarkupRule' => 'infrastructure/markup/rule/PhabricatorKeyboardRemarkupRule.php',
'PhabricatorKeyring' => 'applications/files/keyring/PhabricatorKeyring.php',
'PhabricatorKeyringConfigOptionType' => 'applications/files/keyring/PhabricatorKeyringConfigOptionType.php',
'PhabricatorLDAPAuthProvider' => 'applications/auth/provider/PhabricatorLDAPAuthProvider.php',
'PhabricatorLabelProfileMenuItem' => 'applications/search/menuitem/PhabricatorLabelProfileMenuItem.php',
'PhabricatorLanguageSettingsPanel' => 'applications/settings/panel/PhabricatorLanguageSettingsPanel.php',
'PhabricatorLegalpadApplication' => 'applications/legalpad/application/PhabricatorLegalpadApplication.php',
'PhabricatorLegalpadDocumentPHIDType' => 'applications/legalpad/phid/PhabricatorLegalpadDocumentPHIDType.php',
'PhabricatorLegalpadSignaturePolicyRule' => 'applications/legalpad/policyrule/PhabricatorLegalpadSignaturePolicyRule.php',
'PhabricatorLibraryTestCase' => '__tests__/PhabricatorLibraryTestCase.php',
'PhabricatorLinkProfileMenuItem' => 'applications/search/menuitem/PhabricatorLinkProfileMenuItem.php',
'PhabricatorLipsumArtist' => 'applications/lipsum/image/PhabricatorLipsumArtist.php',
'PhabricatorLipsumContentSource' => 'infrastructure/contentsource/PhabricatorLipsumContentSource.php',
'PhabricatorLipsumGenerateWorkflow' => 'applications/lipsum/management/PhabricatorLipsumGenerateWorkflow.php',
'PhabricatorLipsumManagementWorkflow' => 'applications/lipsum/management/PhabricatorLipsumManagementWorkflow.php',
'PhabricatorLipsumMondrianArtist' => 'applications/lipsum/image/PhabricatorLipsumMondrianArtist.php',
'PhabricatorLiskDAO' => 'infrastructure/storage/lisk/PhabricatorLiskDAO.php',
'PhabricatorLiskExportEngineExtension' => 'infrastructure/export/engine/PhabricatorLiskExportEngineExtension.php',
'PhabricatorLiskFulltextEngineExtension' => 'applications/search/engineextension/PhabricatorLiskFulltextEngineExtension.php',
'PhabricatorLiskSearchEngineExtension' => 'applications/search/engineextension/PhabricatorLiskSearchEngineExtension.php',
'PhabricatorLiskSerializer' => 'infrastructure/storage/lisk/PhabricatorLiskSerializer.php',
'PhabricatorListExportField' => 'infrastructure/export/field/PhabricatorListExportField.php',
'PhabricatorLocalDiskFileStorageEngine' => 'applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php',
'PhabricatorLocalTimeTestCase' => 'view/__tests__/PhabricatorLocalTimeTestCase.php',
'PhabricatorLocaleScopeGuard' => 'infrastructure/internationalization/scope/PhabricatorLocaleScopeGuard.php',
'PhabricatorLocaleScopeGuardTestCase' => 'infrastructure/internationalization/scope/__tests__/PhabricatorLocaleScopeGuardTestCase.php',
'PhabricatorLockLogManagementWorkflow' => 'applications/daemon/management/PhabricatorLockLogManagementWorkflow.php',
'PhabricatorLockManagementWorkflow' => 'applications/daemon/management/PhabricatorLockManagementWorkflow.php',
'PhabricatorLogTriggerAction' => 'infrastructure/daemon/workers/action/PhabricatorLogTriggerAction.php',
'PhabricatorLogoutController' => 'applications/auth/controller/PhabricatorLogoutController.php',
'PhabricatorLunarPhasePolicyRule' => 'applications/policy/rule/PhabricatorLunarPhasePolicyRule.php',
'PhabricatorMacroApplication' => 'applications/macro/application/PhabricatorMacroApplication.php',
'PhabricatorMacroAudioBehaviorTransaction' => 'applications/macro/xaction/PhabricatorMacroAudioBehaviorTransaction.php',
'PhabricatorMacroAudioController' => 'applications/macro/controller/PhabricatorMacroAudioController.php',
'PhabricatorMacroAudioTransaction' => 'applications/macro/xaction/PhabricatorMacroAudioTransaction.php',
'PhabricatorMacroController' => 'applications/macro/controller/PhabricatorMacroController.php',
'PhabricatorMacroDatasource' => 'applications/macro/typeahead/PhabricatorMacroDatasource.php',
'PhabricatorMacroDisableController' => 'applications/macro/controller/PhabricatorMacroDisableController.php',
'PhabricatorMacroDisabledTransaction' => 'applications/macro/xaction/PhabricatorMacroDisabledTransaction.php',
'PhabricatorMacroEditController' => 'applications/macro/controller/PhabricatorMacroEditController.php',
'PhabricatorMacroEditEngine' => 'applications/macro/editor/PhabricatorMacroEditEngine.php',
'PhabricatorMacroEditor' => 'applications/macro/editor/PhabricatorMacroEditor.php',
'PhabricatorMacroFileTransaction' => 'applications/macro/xaction/PhabricatorMacroFileTransaction.php',
'PhabricatorMacroListController' => 'applications/macro/controller/PhabricatorMacroListController.php',
'PhabricatorMacroMacroPHIDType' => 'applications/macro/phid/PhabricatorMacroMacroPHIDType.php',
'PhabricatorMacroMailReceiver' => 'applications/macro/mail/PhabricatorMacroMailReceiver.php',
'PhabricatorMacroManageCapability' => 'applications/macro/capability/PhabricatorMacroManageCapability.php',
'PhabricatorMacroMemeController' => 'applications/macro/controller/PhabricatorMacroMemeController.php',
'PhabricatorMacroMemeDialogController' => 'applications/macro/controller/PhabricatorMacroMemeDialogController.php',
'PhabricatorMacroNameTransaction' => 'applications/macro/xaction/PhabricatorMacroNameTransaction.php',
'PhabricatorMacroQuery' => 'applications/macro/query/PhabricatorMacroQuery.php',
'PhabricatorMacroReplyHandler' => 'applications/macro/mail/PhabricatorMacroReplyHandler.php',
'PhabricatorMacroSearchEngine' => 'applications/macro/query/PhabricatorMacroSearchEngine.php',
'PhabricatorMacroTestCase' => 'applications/macro/xaction/__tests__/PhabricatorMacroTestCase.php',
'PhabricatorMacroTransaction' => 'applications/macro/storage/PhabricatorMacroTransaction.php',
'PhabricatorMacroTransactionComment' => 'applications/macro/storage/PhabricatorMacroTransactionComment.php',
'PhabricatorMacroTransactionQuery' => 'applications/macro/query/PhabricatorMacroTransactionQuery.php',
'PhabricatorMacroTransactionType' => 'applications/macro/xaction/PhabricatorMacroTransactionType.php',
'PhabricatorMacroViewController' => 'applications/macro/controller/PhabricatorMacroViewController.php',
'PhabricatorMailAdapter' => 'applications/metamta/adapter/PhabricatorMailAdapter.php',
+ 'PhabricatorMailAdapterTestCase' => 'applications/metamta/adapter/__tests__/PhabricatorMailAdapterTestCase.php',
'PhabricatorMailAmazonSESAdapter' => 'applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php',
'PhabricatorMailAmazonSNSAdapter' => 'applications/metamta/adapter/PhabricatorMailAmazonSNSAdapter.php',
'PhabricatorMailAttachment' => 'applications/metamta/message/PhabricatorMailAttachment.php',
'PhabricatorMailConfigTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMailConfigTestCase.php',
'PhabricatorMailEmailEngine' => 'applications/metamta/engine/PhabricatorMailEmailEngine.php',
'PhabricatorMailEmailHeraldField' => 'applications/metamta/herald/PhabricatorMailEmailHeraldField.php',
'PhabricatorMailEmailHeraldFieldGroup' => 'applications/metamta/herald/PhabricatorMailEmailHeraldFieldGroup.php',
'PhabricatorMailEmailMessage' => 'applications/metamta/message/PhabricatorMailEmailMessage.php',
'PhabricatorMailEmailSubjectHeraldField' => 'applications/metamta/herald/PhabricatorMailEmailSubjectHeraldField.php',
'PhabricatorMailEngineExtension' => 'applications/metamta/engine/PhabricatorMailEngineExtension.php',
'PhabricatorMailExternalMessage' => 'applications/metamta/message/PhabricatorMailExternalMessage.php',
'PhabricatorMailHeader' => 'applications/metamta/message/PhabricatorMailHeader.php',
'PhabricatorMailMailgunAdapter' => 'applications/metamta/adapter/PhabricatorMailMailgunAdapter.php',
'PhabricatorMailManagementListInboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementListInboundWorkflow.php',
'PhabricatorMailManagementListOutboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php',
'PhabricatorMailManagementReceiveTestWorkflow' => 'applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php',
'PhabricatorMailManagementResendWorkflow' => 'applications/metamta/management/PhabricatorMailManagementResendWorkflow.php',
'PhabricatorMailManagementSendTestWorkflow' => 'applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php',
'PhabricatorMailManagementShowInboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementShowInboundWorkflow.php',
'PhabricatorMailManagementShowOutboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php',
'PhabricatorMailManagementUnverifyWorkflow' => 'applications/metamta/management/PhabricatorMailManagementUnverifyWorkflow.php',
'PhabricatorMailManagementVolumeWorkflow' => 'applications/metamta/management/PhabricatorMailManagementVolumeWorkflow.php',
'PhabricatorMailManagementWorkflow' => 'applications/metamta/management/PhabricatorMailManagementWorkflow.php',
'PhabricatorMailMessageEngine' => 'applications/metamta/engine/PhabricatorMailMessageEngine.php',
'PhabricatorMailMustEncryptHeraldAction' => 'applications/metamta/herald/PhabricatorMailMustEncryptHeraldAction.php',
'PhabricatorMailOutboundMailHeraldAdapter' => 'applications/metamta/herald/PhabricatorMailOutboundMailHeraldAdapter.php',
'PhabricatorMailOutboundRoutingHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingHeraldAction.php',
'PhabricatorMailOutboundRoutingSelfEmailHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingSelfEmailHeraldAction.php',
'PhabricatorMailOutboundRoutingSelfNotificationHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingSelfNotificationHeraldAction.php',
'PhabricatorMailOutboundStatus' => 'applications/metamta/constants/PhabricatorMailOutboundStatus.php',
'PhabricatorMailPostmarkAdapter' => 'applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php',
'PhabricatorMailPropertiesDestructionEngineExtension' => 'applications/metamta/engineextension/PhabricatorMailPropertiesDestructionEngineExtension.php',
'PhabricatorMailReceiver' => 'applications/metamta/receiver/PhabricatorMailReceiver.php',
'PhabricatorMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php',
'PhabricatorMailReplyHandler' => 'applications/metamta/replyhandler/PhabricatorMailReplyHandler.php',
'PhabricatorMailRoutingRule' => 'applications/metamta/constants/PhabricatorMailRoutingRule.php',
'PhabricatorMailSMSEngine' => 'applications/metamta/engine/PhabricatorMailSMSEngine.php',
'PhabricatorMailSMSMessage' => 'applications/metamta/message/PhabricatorMailSMSMessage.php',
'PhabricatorMailSMTPAdapter' => 'applications/metamta/adapter/PhabricatorMailSMTPAdapter.php',
'PhabricatorMailSendGridAdapter' => 'applications/metamta/adapter/PhabricatorMailSendGridAdapter.php',
'PhabricatorMailSendmailAdapter' => 'applications/metamta/adapter/PhabricatorMailSendmailAdapter.php',
'PhabricatorMailSetupCheck' => 'applications/config/check/PhabricatorMailSetupCheck.php',
'PhabricatorMailStamp' => 'applications/metamta/stamp/PhabricatorMailStamp.php',
'PhabricatorMailTarget' => 'applications/metamta/replyhandler/PhabricatorMailTarget.php',
'PhabricatorMailTestAdapter' => 'applications/metamta/adapter/PhabricatorMailTestAdapter.php',
'PhabricatorMailTwilioAdapter' => 'applications/metamta/adapter/PhabricatorMailTwilioAdapter.php',
'PhabricatorMailUtil' => 'applications/metamta/util/PhabricatorMailUtil.php',
'PhabricatorMainMenuBarExtension' => 'view/page/menu/PhabricatorMainMenuBarExtension.php',
'PhabricatorMainMenuSearchView' => 'view/page/menu/PhabricatorMainMenuSearchView.php',
'PhabricatorMainMenuView' => 'view/page/menu/PhabricatorMainMenuView.php',
'PhabricatorManageProfileMenuItem' => 'applications/search/menuitem/PhabricatorManageProfileMenuItem.php',
'PhabricatorManagementWorkflow' => 'infrastructure/management/PhabricatorManagementWorkflow.php',
'PhabricatorManiphestApplication' => 'applications/maniphest/application/PhabricatorManiphestApplication.php',
'PhabricatorManiphestConfigOptions' => 'applications/maniphest/config/PhabricatorManiphestConfigOptions.php',
'PhabricatorManiphestTaskFactEngine' => 'applications/fact/engine/PhabricatorManiphestTaskFactEngine.php',
'PhabricatorManiphestTaskTestDataGenerator' => 'applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php',
'PhabricatorManualActivitySetupCheck' => 'applications/config/check/PhabricatorManualActivitySetupCheck.php',
'PhabricatorMarkupCache' => 'applications/cache/storage/PhabricatorMarkupCache.php',
'PhabricatorMarkupEngine' => 'infrastructure/markup/PhabricatorMarkupEngine.php',
'PhabricatorMarkupEngineTestCase' => 'infrastructure/markup/__tests__/PhabricatorMarkupEngineTestCase.php',
'PhabricatorMarkupInterface' => 'infrastructure/markup/PhabricatorMarkupInterface.php',
'PhabricatorMarkupOneOff' => 'infrastructure/markup/PhabricatorMarkupOneOff.php',
'PhabricatorMarkupPreviewController' => 'infrastructure/markup/PhabricatorMarkupPreviewController.php',
'PhabricatorMemeEngine' => 'applications/macro/engine/PhabricatorMemeEngine.php',
'PhabricatorMemeRemarkupRule' => 'applications/macro/markup/PhabricatorMemeRemarkupRule.php',
'PhabricatorMentionRemarkupRule' => 'applications/people/markup/PhabricatorMentionRemarkupRule.php',
'PhabricatorMentionableInterface' => 'applications/transactions/interface/PhabricatorMentionableInterface.php',
'PhabricatorMercurialGraphStream' => 'applications/repository/daemon/PhabricatorMercurialGraphStream.php',
'PhabricatorMetaMTAActor' => 'applications/metamta/query/PhabricatorMetaMTAActor.php',
'PhabricatorMetaMTAActorQuery' => 'applications/metamta/query/PhabricatorMetaMTAActorQuery.php',
'PhabricatorMetaMTAApplication' => 'applications/metamta/application/PhabricatorMetaMTAApplication.php',
'PhabricatorMetaMTAApplicationEmail' => 'applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php',
'PhabricatorMetaMTAApplicationEmailDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAApplicationEmailDatasource.php',
'PhabricatorMetaMTAApplicationEmailEditor' => 'applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php',
'PhabricatorMetaMTAApplicationEmailHeraldField' => 'applications/metamta/herald/PhabricatorMetaMTAApplicationEmailHeraldField.php',
'PhabricatorMetaMTAApplicationEmailPHIDType' => 'applications/phid/PhabricatorMetaMTAApplicationEmailPHIDType.php',
'PhabricatorMetaMTAApplicationEmailPanel' => 'applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php',
'PhabricatorMetaMTAApplicationEmailQuery' => 'applications/metamta/query/PhabricatorMetaMTAApplicationEmailQuery.php',
'PhabricatorMetaMTAApplicationEmailTransaction' => 'applications/metamta/storage/PhabricatorMetaMTAApplicationEmailTransaction.php',
'PhabricatorMetaMTAApplicationEmailTransactionQuery' => 'applications/metamta/query/PhabricatorMetaMTAApplicationEmailTransactionQuery.php',
'PhabricatorMetaMTAConfigOptions' => 'applications/config/option/PhabricatorMetaMTAConfigOptions.php',
'PhabricatorMetaMTAController' => 'applications/metamta/controller/PhabricatorMetaMTAController.php',
'PhabricatorMetaMTADAO' => 'applications/metamta/storage/PhabricatorMetaMTADAO.php',
'PhabricatorMetaMTAEmailBodyParser' => 'applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php',
'PhabricatorMetaMTAEmailBodyParserTestCase' => 'applications/metamta/parser/__tests__/PhabricatorMetaMTAEmailBodyParserTestCase.php',
'PhabricatorMetaMTAEmailHeraldAction' => 'applications/metamta/herald/PhabricatorMetaMTAEmailHeraldAction.php',
'PhabricatorMetaMTAEmailOthersHeraldAction' => 'applications/metamta/herald/PhabricatorMetaMTAEmailOthersHeraldAction.php',
'PhabricatorMetaMTAEmailSelfHeraldAction' => 'applications/metamta/herald/PhabricatorMetaMTAEmailSelfHeraldAction.php',
'PhabricatorMetaMTAErrorMailAction' => 'applications/metamta/action/PhabricatorMetaMTAErrorMailAction.php',
'PhabricatorMetaMTAMail' => 'applications/metamta/storage/PhabricatorMetaMTAMail.php',
'PhabricatorMetaMTAMailBody' => 'applications/metamta/view/PhabricatorMetaMTAMailBody.php',
'PhabricatorMetaMTAMailBodyTestCase' => 'applications/metamta/view/__tests__/PhabricatorMetaMTAMailBodyTestCase.php',
'PhabricatorMetaMTAMailHasRecipientEdgeType' => 'applications/metamta/edge/PhabricatorMetaMTAMailHasRecipientEdgeType.php',
'PhabricatorMetaMTAMailListController' => 'applications/metamta/controller/PhabricatorMetaMTAMailListController.php',
'PhabricatorMetaMTAMailPHIDType' => 'applications/metamta/phid/PhabricatorMetaMTAMailPHIDType.php',
'PhabricatorMetaMTAMailProperties' => 'applications/metamta/storage/PhabricatorMetaMTAMailProperties.php',
'PhabricatorMetaMTAMailPropertiesQuery' => 'applications/metamta/query/PhabricatorMetaMTAMailPropertiesQuery.php',
'PhabricatorMetaMTAMailQuery' => 'applications/metamta/query/PhabricatorMetaMTAMailQuery.php',
'PhabricatorMetaMTAMailSearchEngine' => 'applications/metamta/query/PhabricatorMetaMTAMailSearchEngine.php',
'PhabricatorMetaMTAMailSection' => 'applications/metamta/view/PhabricatorMetaMTAMailSection.php',
'PhabricatorMetaMTAMailTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php',
'PhabricatorMetaMTAMailViewController' => 'applications/metamta/controller/PhabricatorMetaMTAMailViewController.php',
'PhabricatorMetaMTAMailableDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAMailableDatasource.php',
'PhabricatorMetaMTAMailableFunctionDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAMailableFunctionDatasource.php',
'PhabricatorMetaMTAMailgunReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php',
'PhabricatorMetaMTAMemberQuery' => 'applications/metamta/query/PhabricatorMetaMTAMemberQuery.php',
'PhabricatorMetaMTAPermanentFailureException' => 'applications/metamta/exception/PhabricatorMetaMTAPermanentFailureException.php',
'PhabricatorMetaMTAPostmarkReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTAPostmarkReceiveController.php',
'PhabricatorMetaMTAReceivedMail' => 'applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php',
'PhabricatorMetaMTAReceivedMailProcessingException' => 'applications/metamta/exception/PhabricatorMetaMTAReceivedMailProcessingException.php',
'PhabricatorMetaMTAReceivedMailTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMetaMTAReceivedMailTestCase.php',
'PhabricatorMetaMTASchemaSpec' => 'applications/metamta/storage/PhabricatorMetaMTASchemaSpec.php',
'PhabricatorMetaMTASendGridReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php',
'PhabricatorMetaMTAWorker' => 'applications/metamta/PhabricatorMetaMTAWorker.php',
+ 'PhabricatorMetronome' => 'infrastructure/util/PhabricatorMetronome.php',
+ 'PhabricatorMetronomeTestCase' => 'infrastructure/util/__tests__/PhabricatorMetronomeTestCase.php',
'PhabricatorMetronomicTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorMetronomicTriggerClock.php',
'PhabricatorModularTransaction' => 'applications/transactions/storage/PhabricatorModularTransaction.php',
'PhabricatorModularTransactionType' => 'applications/transactions/storage/PhabricatorModularTransactionType.php',
'PhabricatorMonogramDatasourceEngineExtension' => 'applications/typeahead/engineextension/PhabricatorMonogramDatasourceEngineExtension.php',
'PhabricatorMonospacedFontSetting' => 'applications/settings/setting/PhabricatorMonospacedFontSetting.php',
'PhabricatorMonospacedTextareasSetting' => 'applications/settings/setting/PhabricatorMonospacedTextareasSetting.php',
'PhabricatorMotivatorProfileMenuItem' => 'applications/search/menuitem/PhabricatorMotivatorProfileMenuItem.php',
'PhabricatorMultiColumnUIExample' => 'applications/uiexample/examples/PhabricatorMultiColumnUIExample.php',
'PhabricatorMultiFactorSettingsPanel' => 'applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php',
'PhabricatorMultimeterApplication' => 'applications/multimeter/application/PhabricatorMultimeterApplication.php',
'PhabricatorMustVerifyEmailController' => 'applications/auth/controller/PhabricatorMustVerifyEmailController.php',
'PhabricatorMutedByEdgeType' => 'applications/transactions/edges/PhabricatorMutedByEdgeType.php',
'PhabricatorMutedEdgeType' => 'applications/transactions/edges/PhabricatorMutedEdgeType.php',
'PhabricatorMySQLConfigOptions' => 'applications/config/option/PhabricatorMySQLConfigOptions.php',
'PhabricatorMySQLFileStorageEngine' => 'applications/files/engine/PhabricatorMySQLFileStorageEngine.php',
'PhabricatorMySQLSearchHost' => 'infrastructure/cluster/search/PhabricatorMySQLSearchHost.php',
'PhabricatorMySQLSetupCheck' => 'applications/config/check/PhabricatorMySQLSetupCheck.php',
'PhabricatorNamedQuery' => 'applications/search/storage/PhabricatorNamedQuery.php',
'PhabricatorNamedQueryConfig' => 'applications/search/storage/PhabricatorNamedQueryConfig.php',
'PhabricatorNamedQueryConfigQuery' => 'applications/search/query/PhabricatorNamedQueryConfigQuery.php',
'PhabricatorNamedQueryQuery' => 'applications/search/query/PhabricatorNamedQueryQuery.php',
'PhabricatorNavigationRemarkupRule' => 'infrastructure/markup/rule/PhabricatorNavigationRemarkupRule.php',
'PhabricatorNeverTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorNeverTriggerClock.php',
'PhabricatorNgramsIndexEngineExtension' => 'applications/search/engineextension/PhabricatorNgramsIndexEngineExtension.php',
'PhabricatorNgramsInterface' => 'applications/search/interface/PhabricatorNgramsInterface.php',
'PhabricatorNotificationBuilder' => 'applications/notification/builder/PhabricatorNotificationBuilder.php',
'PhabricatorNotificationClearController' => 'applications/notification/controller/PhabricatorNotificationClearController.php',
'PhabricatorNotificationClient' => 'applications/notification/client/PhabricatorNotificationClient.php',
'PhabricatorNotificationConfigOptions' => 'applications/config/option/PhabricatorNotificationConfigOptions.php',
'PhabricatorNotificationController' => 'applications/notification/controller/PhabricatorNotificationController.php',
'PhabricatorNotificationDestructionEngineExtension' => 'applications/notification/engineextension/PhabricatorNotificationDestructionEngineExtension.php',
'PhabricatorNotificationIndividualController' => 'applications/notification/controller/PhabricatorNotificationIndividualController.php',
'PhabricatorNotificationListController' => 'applications/notification/controller/PhabricatorNotificationListController.php',
'PhabricatorNotificationPanelController' => 'applications/notification/controller/PhabricatorNotificationPanelController.php',
'PhabricatorNotificationQuery' => 'applications/notification/query/PhabricatorNotificationQuery.php',
'PhabricatorNotificationSearchEngine' => 'applications/notification/query/PhabricatorNotificationSearchEngine.php',
'PhabricatorNotificationServerRef' => 'applications/notification/client/PhabricatorNotificationServerRef.php',
'PhabricatorNotificationServersConfigType' => 'applications/notification/config/PhabricatorNotificationServersConfigType.php',
'PhabricatorNotificationStatusView' => 'applications/notification/view/PhabricatorNotificationStatusView.php',
'PhabricatorNotificationTestController' => 'applications/notification/controller/PhabricatorNotificationTestController.php',
'PhabricatorNotificationUIExample' => 'applications/uiexample/examples/PhabricatorNotificationUIExample.php',
'PhabricatorNotificationsApplication' => 'applications/notification/application/PhabricatorNotificationsApplication.php',
'PhabricatorNotificationsSetting' => 'applications/settings/setting/PhabricatorNotificationsSetting.php',
'PhabricatorNotificationsSettingsPanel' => 'applications/settings/panel/PhabricatorNotificationsSettingsPanel.php',
'PhabricatorNuanceApplication' => 'applications/nuance/application/PhabricatorNuanceApplication.php',
'PhabricatorOAuth1AuthProvider' => 'applications/auth/provider/PhabricatorOAuth1AuthProvider.php',
'PhabricatorOAuth1SecretTemporaryTokenType' => 'applications/auth/provider/PhabricatorOAuth1SecretTemporaryTokenType.php',
'PhabricatorOAuth2AuthProvider' => 'applications/auth/provider/PhabricatorOAuth2AuthProvider.php',
'PhabricatorOAuthAuthProvider' => 'applications/auth/provider/PhabricatorOAuthAuthProvider.php',
'PhabricatorOAuthClientAuthorization' => 'applications/oauthserver/storage/PhabricatorOAuthClientAuthorization.php',
'PhabricatorOAuthClientAuthorizationQuery' => 'applications/oauthserver/query/PhabricatorOAuthClientAuthorizationQuery.php',
'PhabricatorOAuthClientController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientController.php',
'PhabricatorOAuthClientDisableController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientDisableController.php',
'PhabricatorOAuthClientEditController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientEditController.php',
'PhabricatorOAuthClientListController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientListController.php',
'PhabricatorOAuthClientSecretController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientSecretController.php',
'PhabricatorOAuthClientTestController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientTestController.php',
'PhabricatorOAuthClientViewController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientViewController.php',
'PhabricatorOAuthResponse' => 'applications/oauthserver/PhabricatorOAuthResponse.php',
'PhabricatorOAuthServer' => 'applications/oauthserver/PhabricatorOAuthServer.php',
'PhabricatorOAuthServerAccessToken' => 'applications/oauthserver/storage/PhabricatorOAuthServerAccessToken.php',
'PhabricatorOAuthServerApplication' => 'applications/oauthserver/application/PhabricatorOAuthServerApplication.php',
'PhabricatorOAuthServerAuthController' => 'applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php',
'PhabricatorOAuthServerAuthorizationCode' => 'applications/oauthserver/storage/PhabricatorOAuthServerAuthorizationCode.php',
'PhabricatorOAuthServerAuthorizationsSettingsPanel' => 'applications/oauthserver/panel/PhabricatorOAuthServerAuthorizationsSettingsPanel.php',
'PhabricatorOAuthServerClient' => 'applications/oauthserver/storage/PhabricatorOAuthServerClient.php',
'PhabricatorOAuthServerClientAuthorizationPHIDType' => 'applications/oauthserver/phid/PhabricatorOAuthServerClientAuthorizationPHIDType.php',
'PhabricatorOAuthServerClientPHIDType' => 'applications/oauthserver/phid/PhabricatorOAuthServerClientPHIDType.php',
'PhabricatorOAuthServerClientQuery' => 'applications/oauthserver/query/PhabricatorOAuthServerClientQuery.php',
'PhabricatorOAuthServerClientSearchEngine' => 'applications/oauthserver/query/PhabricatorOAuthServerClientSearchEngine.php',
'PhabricatorOAuthServerController' => 'applications/oauthserver/controller/PhabricatorOAuthServerController.php',
'PhabricatorOAuthServerCreateClientsCapability' => 'applications/oauthserver/capability/PhabricatorOAuthServerCreateClientsCapability.php',
'PhabricatorOAuthServerDAO' => 'applications/oauthserver/storage/PhabricatorOAuthServerDAO.php',
'PhabricatorOAuthServerEditEngine' => 'applications/oauthserver/editor/PhabricatorOAuthServerEditEngine.php',
'PhabricatorOAuthServerEditor' => 'applications/oauthserver/editor/PhabricatorOAuthServerEditor.php',
'PhabricatorOAuthServerSchemaSpec' => 'applications/oauthserver/query/PhabricatorOAuthServerSchemaSpec.php',
'PhabricatorOAuthServerScope' => 'applications/oauthserver/PhabricatorOAuthServerScope.php',
'PhabricatorOAuthServerTestCase' => 'applications/oauthserver/__tests__/PhabricatorOAuthServerTestCase.php',
'PhabricatorOAuthServerTokenController' => 'applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php',
'PhabricatorOAuthServerTransaction' => 'applications/oauthserver/storage/PhabricatorOAuthServerTransaction.php',
'PhabricatorOAuthServerTransactionQuery' => 'applications/oauthserver/query/PhabricatorOAuthServerTransactionQuery.php',
'PhabricatorObjectGraph' => 'infrastructure/graph/PhabricatorObjectGraph.php',
'PhabricatorObjectHandle' => 'applications/phid/PhabricatorObjectHandle.php',
'PhabricatorObjectHasAsanaSubtaskEdgeType' => 'applications/doorkeeper/edge/PhabricatorObjectHasAsanaSubtaskEdgeType.php',
'PhabricatorObjectHasAsanaTaskEdgeType' => 'applications/doorkeeper/edge/PhabricatorObjectHasAsanaTaskEdgeType.php',
'PhabricatorObjectHasContributorEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasContributorEdgeType.php',
'PhabricatorObjectHasDraftEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasDraftEdgeType.php',
'PhabricatorObjectHasFileEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasFileEdgeType.php',
'PhabricatorObjectHasJiraIssueEdgeType' => 'applications/doorkeeper/edge/PhabricatorObjectHasJiraIssueEdgeType.php',
'PhabricatorObjectHasSubscriberEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasSubscriberEdgeType.php',
'PhabricatorObjectHasUnsubscriberEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasUnsubscriberEdgeType.php',
'PhabricatorObjectHasWatcherEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasWatcherEdgeType.php',
'PhabricatorObjectListQuery' => 'applications/phid/query/PhabricatorObjectListQuery.php',
'PhabricatorObjectListQueryTestCase' => 'applications/phid/query/__tests__/PhabricatorObjectListQueryTestCase.php',
'PhabricatorObjectMailReceiver' => 'applications/metamta/receiver/PhabricatorObjectMailReceiver.php',
'PhabricatorObjectMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorObjectMailReceiverTestCase.php',
'PhabricatorObjectMentionedByObjectEdgeType' => 'applications/transactions/edges/PhabricatorObjectMentionedByObjectEdgeType.php',
'PhabricatorObjectMentionsObjectEdgeType' => 'applications/transactions/edges/PhabricatorObjectMentionsObjectEdgeType.php',
'PhabricatorObjectQuery' => 'applications/phid/query/PhabricatorObjectQuery.php',
'PhabricatorObjectRelationship' => 'applications/search/relationship/PhabricatorObjectRelationship.php',
'PhabricatorObjectRelationshipList' => 'applications/search/relationship/PhabricatorObjectRelationshipList.php',
'PhabricatorObjectRelationshipSource' => 'applications/search/relationship/PhabricatorObjectRelationshipSource.php',
'PhabricatorObjectRemarkupRule' => 'infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php',
'PhabricatorObjectSelectorDialog' => 'view/control/PhabricatorObjectSelectorDialog.php',
'PhabricatorObjectStatus' => 'infrastructure/status/PhabricatorObjectStatus.php',
'PhabricatorOffsetPagedQuery' => 'infrastructure/query/PhabricatorOffsetPagedQuery.php',
'PhabricatorOldWorldContentSource' => 'infrastructure/contentsource/PhabricatorOldWorldContentSource.php',
'PhabricatorOlderInlinesSetting' => 'applications/settings/setting/PhabricatorOlderInlinesSetting.php',
'PhabricatorOneTimeTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorOneTimeTriggerClock.php',
'PhabricatorOpcodeCacheSpec' => 'applications/cache/spec/PhabricatorOpcodeCacheSpec.php',
+ 'PhabricatorOptionExportField' => 'infrastructure/export/field/PhabricatorOptionExportField.php',
'PhabricatorOptionGroupSetting' => 'applications/settings/setting/PhabricatorOptionGroupSetting.php',
'PhabricatorOwnerPathQuery' => 'applications/owners/query/PhabricatorOwnerPathQuery.php',
'PhabricatorOwnersApplication' => 'applications/owners/application/PhabricatorOwnersApplication.php',
'PhabricatorOwnersArchiveController' => 'applications/owners/controller/PhabricatorOwnersArchiveController.php',
+ 'PhabricatorOwnersAuditRule' => 'applications/owners/constants/PhabricatorOwnersAuditRule.php',
'PhabricatorOwnersConfigOptions' => 'applications/owners/config/PhabricatorOwnersConfigOptions.php',
'PhabricatorOwnersConfiguredCustomField' => 'applications/owners/customfield/PhabricatorOwnersConfiguredCustomField.php',
'PhabricatorOwnersController' => 'applications/owners/controller/PhabricatorOwnersController.php',
'PhabricatorOwnersCustomField' => 'applications/owners/customfield/PhabricatorOwnersCustomField.php',
'PhabricatorOwnersCustomFieldNumericIndex' => 'applications/owners/storage/PhabricatorOwnersCustomFieldNumericIndex.php',
'PhabricatorOwnersCustomFieldStorage' => 'applications/owners/storage/PhabricatorOwnersCustomFieldStorage.php',
'PhabricatorOwnersCustomFieldStringIndex' => 'applications/owners/storage/PhabricatorOwnersCustomFieldStringIndex.php',
'PhabricatorOwnersDAO' => 'applications/owners/storage/PhabricatorOwnersDAO.php',
'PhabricatorOwnersDefaultEditCapability' => 'applications/owners/capability/PhabricatorOwnersDefaultEditCapability.php',
'PhabricatorOwnersDefaultViewCapability' => 'applications/owners/capability/PhabricatorOwnersDefaultViewCapability.php',
'PhabricatorOwnersDetailController' => 'applications/owners/controller/PhabricatorOwnersDetailController.php',
'PhabricatorOwnersEditController' => 'applications/owners/controller/PhabricatorOwnersEditController.php',
'PhabricatorOwnersHovercardEngineExtension' => 'applications/owners/engineextension/PhabricatorOwnersHovercardEngineExtension.php',
'PhabricatorOwnersListController' => 'applications/owners/controller/PhabricatorOwnersListController.php',
'PhabricatorOwnersOwner' => 'applications/owners/storage/PhabricatorOwnersOwner.php',
'PhabricatorOwnersPackage' => 'applications/owners/storage/PhabricatorOwnersPackage.php',
'PhabricatorOwnersPackageAuditingTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php',
'PhabricatorOwnersPackageAutoreviewTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageAutoreviewTransaction.php',
'PhabricatorOwnersPackageContextFreeGrammar' => 'applications/owners/lipsum/PhabricatorOwnersPackageContextFreeGrammar.php',
'PhabricatorOwnersPackageDatasource' => 'applications/owners/typeahead/PhabricatorOwnersPackageDatasource.php',
'PhabricatorOwnersPackageDescriptionTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageDescriptionTransaction.php',
'PhabricatorOwnersPackageDominionTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageDominionTransaction.php',
'PhabricatorOwnersPackageEditEngine' => 'applications/owners/editor/PhabricatorOwnersPackageEditEngine.php',
'PhabricatorOwnersPackageFerretEngine' => 'applications/owners/search/PhabricatorOwnersPackageFerretEngine.php',
'PhabricatorOwnersPackageFulltextEngine' => 'applications/owners/search/PhabricatorOwnersPackageFulltextEngine.php',
'PhabricatorOwnersPackageFunctionDatasource' => 'applications/owners/typeahead/PhabricatorOwnersPackageFunctionDatasource.php',
'PhabricatorOwnersPackageIgnoredTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageIgnoredTransaction.php',
'PhabricatorOwnersPackageNameNgrams' => 'applications/owners/storage/PhabricatorOwnersPackageNameNgrams.php',
'PhabricatorOwnersPackageNameTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageNameTransaction.php',
'PhabricatorOwnersPackageOwnerDatasource' => 'applications/owners/typeahead/PhabricatorOwnersPackageOwnerDatasource.php',
'PhabricatorOwnersPackageOwnersTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageOwnersTransaction.php',
'PhabricatorOwnersPackagePHIDType' => 'applications/owners/phid/PhabricatorOwnersPackagePHIDType.php',
'PhabricatorOwnersPackagePathsTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php',
'PhabricatorOwnersPackagePrimaryTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackagePrimaryTransaction.php',
'PhabricatorOwnersPackageQuery' => 'applications/owners/query/PhabricatorOwnersPackageQuery.php',
'PhabricatorOwnersPackageRemarkupRule' => 'applications/owners/remarkup/PhabricatorOwnersPackageRemarkupRule.php',
'PhabricatorOwnersPackageSearchEngine' => 'applications/owners/query/PhabricatorOwnersPackageSearchEngine.php',
'PhabricatorOwnersPackageStatusTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageStatusTransaction.php',
'PhabricatorOwnersPackageTestCase' => 'applications/owners/storage/__tests__/PhabricatorOwnersPackageTestCase.php',
'PhabricatorOwnersPackageTestDataGenerator' => 'applications/owners/lipsum/PhabricatorOwnersPackageTestDataGenerator.php',
'PhabricatorOwnersPackageTransaction' => 'applications/owners/storage/PhabricatorOwnersPackageTransaction.php',
'PhabricatorOwnersPackageTransactionEditor' => 'applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php',
'PhabricatorOwnersPackageTransactionQuery' => 'applications/owners/query/PhabricatorOwnersPackageTransactionQuery.php',
'PhabricatorOwnersPackageTransactionType' => 'applications/owners/xaction/PhabricatorOwnersPackageTransactionType.php',
'PhabricatorOwnersPath' => 'applications/owners/storage/PhabricatorOwnersPath.php',
'PhabricatorOwnersPathContextFreeGrammar' => 'applications/owners/lipsum/PhabricatorOwnersPathContextFreeGrammar.php',
'PhabricatorOwnersPathsController' => 'applications/owners/controller/PhabricatorOwnersPathsController.php',
'PhabricatorOwnersPathsSearchEngineAttachment' => 'applications/owners/engineextension/PhabricatorOwnersPathsSearchEngineAttachment.php',
'PhabricatorOwnersSchemaSpec' => 'applications/owners/storage/PhabricatorOwnersSchemaSpec.php',
'PhabricatorOwnersSearchField' => 'applications/owners/searchfield/PhabricatorOwnersSearchField.php',
'PhabricatorPDFDocumentEngine' => 'applications/files/document/PhabricatorPDFDocumentEngine.php',
'PhabricatorPHDConfigOptions' => 'applications/config/option/PhabricatorPHDConfigOptions.php',
'PhabricatorPHID' => 'applications/phid/storage/PhabricatorPHID.php',
'PhabricatorPHIDConstants' => 'applications/phid/PhabricatorPHIDConstants.php',
'PhabricatorPHIDExportField' => 'infrastructure/export/field/PhabricatorPHIDExportField.php',
'PhabricatorPHIDInterface' => 'applications/phid/interface/PhabricatorPHIDInterface.php',
'PhabricatorPHIDListEditField' => 'applications/transactions/editfield/PhabricatorPHIDListEditField.php',
'PhabricatorPHIDListEditType' => 'applications/transactions/edittype/PhabricatorPHIDListEditType.php',
'PhabricatorPHIDListExportField' => 'infrastructure/export/field/PhabricatorPHIDListExportField.php',
'PhabricatorPHIDMailStamp' => 'applications/metamta/stamp/PhabricatorPHIDMailStamp.php',
'PhabricatorPHIDResolver' => 'applications/phid/resolver/PhabricatorPHIDResolver.php',
'PhabricatorPHIDType' => 'applications/phid/type/PhabricatorPHIDType.php',
'PhabricatorPHIDTypeTestCase' => 'applications/phid/type/__tests__/PhabricatorPHIDTypeTestCase.php',
'PhabricatorPHIDsSearchField' => 'applications/search/field/PhabricatorPHIDsSearchField.php',
'PhabricatorPHPASTApplication' => 'applications/phpast/application/PhabricatorPHPASTApplication.php',
'PhabricatorPHPConfigSetupCheck' => 'applications/config/check/PhabricatorPHPConfigSetupCheck.php',
'PhabricatorPHPPreflightSetupCheck' => 'applications/config/check/PhabricatorPHPPreflightSetupCheck.php',
'PhabricatorPackagesApplication' => 'applications/packages/application/PhabricatorPackagesApplication.php',
'PhabricatorPackagesController' => 'applications/packages/controller/PhabricatorPackagesController.php',
'PhabricatorPackagesCreatePublisherCapability' => 'applications/packages/capability/PhabricatorPackagesCreatePublisherCapability.php',
'PhabricatorPackagesDAO' => 'applications/packages/storage/PhabricatorPackagesDAO.php',
'PhabricatorPackagesEditEngine' => 'applications/packages/editor/PhabricatorPackagesEditEngine.php',
'PhabricatorPackagesEditor' => 'applications/packages/editor/PhabricatorPackagesEditor.php',
'PhabricatorPackagesNgrams' => 'applications/packages/storage/PhabricatorPackagesNgrams.php',
'PhabricatorPackagesPackage' => 'applications/packages/storage/PhabricatorPackagesPackage.php',
'PhabricatorPackagesPackageController' => 'applications/packages/controller/PhabricatorPackagesPackageController.php',
'PhabricatorPackagesPackageDatasource' => 'applications/packages/typeahead/PhabricatorPackagesPackageDatasource.php',
'PhabricatorPackagesPackageDefaultEditCapability' => 'applications/packages/capability/PhabricatorPackagesPackageDefaultEditCapability.php',
'PhabricatorPackagesPackageDefaultViewCapability' => 'applications/packages/capability/PhabricatorPackagesPackageDefaultViewCapability.php',
'PhabricatorPackagesPackageEditConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesPackageEditConduitAPIMethod.php',
'PhabricatorPackagesPackageEditController' => 'applications/packages/controller/PhabricatorPackagesPackageEditController.php',
'PhabricatorPackagesPackageEditEngine' => 'applications/packages/editor/PhabricatorPackagesPackageEditEngine.php',
'PhabricatorPackagesPackageEditor' => 'applications/packages/editor/PhabricatorPackagesPackageEditor.php',
'PhabricatorPackagesPackageKeyTransaction' => 'applications/packages/xaction/package/PhabricatorPackagesPackageKeyTransaction.php',
'PhabricatorPackagesPackageListController' => 'applications/packages/controller/PhabricatorPackagesPackageListController.php',
'PhabricatorPackagesPackageListView' => 'applications/packages/view/PhabricatorPackagesPackageListView.php',
'PhabricatorPackagesPackageNameNgrams' => 'applications/packages/storage/PhabricatorPackagesPackageNameNgrams.php',
'PhabricatorPackagesPackageNameTransaction' => 'applications/packages/xaction/package/PhabricatorPackagesPackageNameTransaction.php',
'PhabricatorPackagesPackagePHIDType' => 'applications/packages/phid/PhabricatorPackagesPackagePHIDType.php',
'PhabricatorPackagesPackagePublisherTransaction' => 'applications/packages/xaction/package/PhabricatorPackagesPackagePublisherTransaction.php',
'PhabricatorPackagesPackageQuery' => 'applications/packages/query/PhabricatorPackagesPackageQuery.php',
'PhabricatorPackagesPackageSearchConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesPackageSearchConduitAPIMethod.php',
'PhabricatorPackagesPackageSearchEngine' => 'applications/packages/query/PhabricatorPackagesPackageSearchEngine.php',
'PhabricatorPackagesPackageTransaction' => 'applications/packages/storage/PhabricatorPackagesPackageTransaction.php',
'PhabricatorPackagesPackageTransactionQuery' => 'applications/packages/query/PhabricatorPackagesPackageTransactionQuery.php',
'PhabricatorPackagesPackageTransactionType' => 'applications/packages/xaction/package/PhabricatorPackagesPackageTransactionType.php',
'PhabricatorPackagesPackageViewController' => 'applications/packages/controller/PhabricatorPackagesPackageViewController.php',
'PhabricatorPackagesPublisher' => 'applications/packages/storage/PhabricatorPackagesPublisher.php',
'PhabricatorPackagesPublisherController' => 'applications/packages/controller/PhabricatorPackagesPublisherController.php',
'PhabricatorPackagesPublisherDatasource' => 'applications/packages/typeahead/PhabricatorPackagesPublisherDatasource.php',
'PhabricatorPackagesPublisherDefaultEditCapability' => 'applications/packages/capability/PhabricatorPackagesPublisherDefaultEditCapability.php',
'PhabricatorPackagesPublisherEditConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesPublisherEditConduitAPIMethod.php',
'PhabricatorPackagesPublisherEditController' => 'applications/packages/controller/PhabricatorPackagesPublisherEditController.php',
'PhabricatorPackagesPublisherEditEngine' => 'applications/packages/editor/PhabricatorPackagesPublisherEditEngine.php',
'PhabricatorPackagesPublisherEditor' => 'applications/packages/editor/PhabricatorPackagesPublisherEditor.php',
'PhabricatorPackagesPublisherKeyTransaction' => 'applications/packages/xaction/publisher/PhabricatorPackagesPublisherKeyTransaction.php',
'PhabricatorPackagesPublisherListController' => 'applications/packages/controller/PhabricatorPackagesPublisherListController.php',
'PhabricatorPackagesPublisherListView' => 'applications/packages/view/PhabricatorPackagesPublisherListView.php',
'PhabricatorPackagesPublisherNameNgrams' => 'applications/packages/storage/PhabricatorPackagesPublisherNameNgrams.php',
'PhabricatorPackagesPublisherNameTransaction' => 'applications/packages/xaction/publisher/PhabricatorPackagesPublisherNameTransaction.php',
'PhabricatorPackagesPublisherPHIDType' => 'applications/packages/phid/PhabricatorPackagesPublisherPHIDType.php',
'PhabricatorPackagesPublisherQuery' => 'applications/packages/query/PhabricatorPackagesPublisherQuery.php',
'PhabricatorPackagesPublisherSearchConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesPublisherSearchConduitAPIMethod.php',
'PhabricatorPackagesPublisherSearchEngine' => 'applications/packages/query/PhabricatorPackagesPublisherSearchEngine.php',
'PhabricatorPackagesPublisherTransaction' => 'applications/packages/storage/PhabricatorPackagesPublisherTransaction.php',
'PhabricatorPackagesPublisherTransactionQuery' => 'applications/packages/query/PhabricatorPackagesPublisherTransactionQuery.php',
'PhabricatorPackagesPublisherTransactionType' => 'applications/packages/xaction/publisher/PhabricatorPackagesPublisherTransactionType.php',
'PhabricatorPackagesPublisherViewController' => 'applications/packages/controller/PhabricatorPackagesPublisherViewController.php',
'PhabricatorPackagesQuery' => 'applications/packages/query/PhabricatorPackagesQuery.php',
'PhabricatorPackagesSchemaSpec' => 'applications/packages/storage/PhabricatorPackagesSchemaSpec.php',
'PhabricatorPackagesTransactionType' => 'applications/packages/xaction/PhabricatorPackagesTransactionType.php',
'PhabricatorPackagesVersion' => 'applications/packages/storage/PhabricatorPackagesVersion.php',
'PhabricatorPackagesVersionController' => 'applications/packages/controller/PhabricatorPackagesVersionController.php',
'PhabricatorPackagesVersionEditConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesVersionEditConduitAPIMethod.php',
'PhabricatorPackagesVersionEditController' => 'applications/packages/controller/PhabricatorPackagesVersionEditController.php',
'PhabricatorPackagesVersionEditEngine' => 'applications/packages/editor/PhabricatorPackagesVersionEditEngine.php',
'PhabricatorPackagesVersionEditor' => 'applications/packages/editor/PhabricatorPackagesVersionEditor.php',
'PhabricatorPackagesVersionListController' => 'applications/packages/controller/PhabricatorPackagesVersionListController.php',
'PhabricatorPackagesVersionListView' => 'applications/packages/view/PhabricatorPackagesVersionListView.php',
'PhabricatorPackagesVersionNameNgrams' => 'applications/packages/storage/PhabricatorPackagesVersionNameNgrams.php',
'PhabricatorPackagesVersionNameTransaction' => 'applications/packages/xaction/version/PhabricatorPackagesVersionNameTransaction.php',
'PhabricatorPackagesVersionPHIDType' => 'applications/packages/phid/PhabricatorPackagesVersionPHIDType.php',
'PhabricatorPackagesVersionPackageTransaction' => 'applications/packages/xaction/version/PhabricatorPackagesVersionPackageTransaction.php',
'PhabricatorPackagesVersionQuery' => 'applications/packages/query/PhabricatorPackagesVersionQuery.php',
'PhabricatorPackagesVersionSearchConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesVersionSearchConduitAPIMethod.php',
'PhabricatorPackagesVersionSearchEngine' => 'applications/packages/query/PhabricatorPackagesVersionSearchEngine.php',
'PhabricatorPackagesVersionTransaction' => 'applications/packages/storage/PhabricatorPackagesVersionTransaction.php',
'PhabricatorPackagesVersionTransactionQuery' => 'applications/packages/query/PhabricatorPackagesVersionTransactionQuery.php',
'PhabricatorPackagesVersionTransactionType' => 'applications/packages/xaction/version/PhabricatorPackagesVersionTransactionType.php',
'PhabricatorPackagesVersionViewController' => 'applications/packages/controller/PhabricatorPackagesVersionViewController.php',
'PhabricatorPackagesView' => 'applications/packages/view/PhabricatorPackagesView.php',
'PhabricatorPagerUIExample' => 'applications/uiexample/examples/PhabricatorPagerUIExample.php',
'PhabricatorPassphraseApplication' => 'applications/passphrase/application/PhabricatorPassphraseApplication.php',
'PhabricatorPasswordAuthProvider' => 'applications/auth/provider/PhabricatorPasswordAuthProvider.php',
'PhabricatorPasswordDestructionEngineExtension' => 'applications/auth/extension/PhabricatorPasswordDestructionEngineExtension.php',
'PhabricatorPasswordHasher' => 'infrastructure/util/password/PhabricatorPasswordHasher.php',
'PhabricatorPasswordHasherTestCase' => 'infrastructure/util/password/__tests__/PhabricatorPasswordHasherTestCase.php',
'PhabricatorPasswordHasherUnavailableException' => 'infrastructure/util/password/PhabricatorPasswordHasherUnavailableException.php',
'PhabricatorPasswordSettingsPanel' => 'applications/settings/panel/PhabricatorPasswordSettingsPanel.php',
'PhabricatorPaste' => 'applications/paste/storage/PhabricatorPaste.php',
'PhabricatorPasteApplication' => 'applications/paste/application/PhabricatorPasteApplication.php',
'PhabricatorPasteArchiveController' => 'applications/paste/controller/PhabricatorPasteArchiveController.php',
'PhabricatorPasteContentSearchEngineAttachment' => 'applications/paste/engineextension/PhabricatorPasteContentSearchEngineAttachment.php',
'PhabricatorPasteContentTransaction' => 'applications/paste/xaction/PhabricatorPasteContentTransaction.php',
'PhabricatorPasteController' => 'applications/paste/controller/PhabricatorPasteController.php',
'PhabricatorPasteDAO' => 'applications/paste/storage/PhabricatorPasteDAO.php',
'PhabricatorPasteEditController' => 'applications/paste/controller/PhabricatorPasteEditController.php',
'PhabricatorPasteEditEngine' => 'applications/paste/editor/PhabricatorPasteEditEngine.php',
'PhabricatorPasteEditor' => 'applications/paste/editor/PhabricatorPasteEditor.php',
'PhabricatorPasteFilenameContextFreeGrammar' => 'applications/paste/lipsum/PhabricatorPasteFilenameContextFreeGrammar.php',
'PhabricatorPasteLanguageTransaction' => 'applications/paste/xaction/PhabricatorPasteLanguageTransaction.php',
'PhabricatorPasteListController' => 'applications/paste/controller/PhabricatorPasteListController.php',
'PhabricatorPastePastePHIDType' => 'applications/paste/phid/PhabricatorPastePastePHIDType.php',
'PhabricatorPasteQuery' => 'applications/paste/query/PhabricatorPasteQuery.php',
'PhabricatorPasteRawController' => 'applications/paste/controller/PhabricatorPasteRawController.php',
'PhabricatorPasteRemarkupRule' => 'applications/paste/remarkup/PhabricatorPasteRemarkupRule.php',
'PhabricatorPasteSchemaSpec' => 'applications/paste/storage/PhabricatorPasteSchemaSpec.php',
'PhabricatorPasteSearchEngine' => 'applications/paste/query/PhabricatorPasteSearchEngine.php',
'PhabricatorPasteSnippet' => 'applications/paste/snippet/PhabricatorPasteSnippet.php',
'PhabricatorPasteStatusTransaction' => 'applications/paste/xaction/PhabricatorPasteStatusTransaction.php',
'PhabricatorPasteTestDataGenerator' => 'applications/paste/lipsum/PhabricatorPasteTestDataGenerator.php',
'PhabricatorPasteTitleTransaction' => 'applications/paste/xaction/PhabricatorPasteTitleTransaction.php',
'PhabricatorPasteTransaction' => 'applications/paste/storage/PhabricatorPasteTransaction.php',
'PhabricatorPasteTransactionComment' => 'applications/paste/storage/PhabricatorPasteTransactionComment.php',
'PhabricatorPasteTransactionQuery' => 'applications/paste/query/PhabricatorPasteTransactionQuery.php',
'PhabricatorPasteTransactionType' => 'applications/paste/xaction/PhabricatorPasteTransactionType.php',
'PhabricatorPasteViewController' => 'applications/paste/controller/PhabricatorPasteViewController.php',
'PhabricatorPathSetupCheck' => 'applications/config/check/PhabricatorPathSetupCheck.php',
'PhabricatorPeopleAnyOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleAnyOwnerDatasource.php',
'PhabricatorPeopleApplication' => 'applications/people/application/PhabricatorPeopleApplication.php',
'PhabricatorPeopleApproveController' => 'applications/people/controller/PhabricatorPeopleApproveController.php',
'PhabricatorPeopleAvailabilitySearchEngineAttachment' => 'applications/people/engineextension/PhabricatorPeopleAvailabilitySearchEngineAttachment.php',
'PhabricatorPeopleBadgesProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleBadgesProfileMenuItem.php',
'PhabricatorPeopleCommitsProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleCommitsProfileMenuItem.php',
'PhabricatorPeopleController' => 'applications/people/controller/PhabricatorPeopleController.php',
'PhabricatorPeopleCreateController' => 'applications/people/controller/PhabricatorPeopleCreateController.php',
'PhabricatorPeopleCreateGuidanceContext' => 'applications/people/guidance/PhabricatorPeopleCreateGuidanceContext.php',
'PhabricatorPeopleDatasource' => 'applications/people/typeahead/PhabricatorPeopleDatasource.php',
'PhabricatorPeopleDatasourceEngineExtension' => 'applications/people/engineextension/PhabricatorPeopleDatasourceEngineExtension.php',
'PhabricatorPeopleDeleteController' => 'applications/people/controller/PhabricatorPeopleDeleteController.php',
'PhabricatorPeopleDetailsProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleDetailsProfileMenuItem.php',
'PhabricatorPeopleDisableController' => 'applications/people/controller/PhabricatorPeopleDisableController.php',
'PhabricatorPeopleEmpowerController' => 'applications/people/controller/PhabricatorPeopleEmpowerController.php',
'PhabricatorPeopleExternalPHIDType' => 'applications/people/phid/PhabricatorPeopleExternalPHIDType.php',
'PhabricatorPeopleIconSet' => 'applications/people/icon/PhabricatorPeopleIconSet.php',
'PhabricatorPeopleInviteController' => 'applications/people/controller/PhabricatorPeopleInviteController.php',
'PhabricatorPeopleInviteListController' => 'applications/people/controller/PhabricatorPeopleInviteListController.php',
'PhabricatorPeopleInviteSendController' => 'applications/people/controller/PhabricatorPeopleInviteSendController.php',
- 'PhabricatorPeopleLdapController' => 'applications/people/controller/PhabricatorPeopleLdapController.php',
'PhabricatorPeopleListController' => 'applications/people/controller/PhabricatorPeopleListController.php',
'PhabricatorPeopleLogQuery' => 'applications/people/query/PhabricatorPeopleLogQuery.php',
'PhabricatorPeopleLogSearchEngine' => 'applications/people/query/PhabricatorPeopleLogSearchEngine.php',
'PhabricatorPeopleLogsController' => 'applications/people/controller/PhabricatorPeopleLogsController.php',
'PhabricatorPeopleMailEngine' => 'applications/people/mail/PhabricatorPeopleMailEngine.php',
'PhabricatorPeopleMailEngineException' => 'applications/people/mail/PhabricatorPeopleMailEngineException.php',
'PhabricatorPeopleManageProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleManageProfileMenuItem.php',
'PhabricatorPeopleManagementWorkflow' => 'applications/people/management/PhabricatorPeopleManagementWorkflow.php',
'PhabricatorPeopleNewController' => 'applications/people/controller/PhabricatorPeopleNewController.php',
'PhabricatorPeopleNoOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleNoOwnerDatasource.php',
'PhabricatorPeopleOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleOwnerDatasource.php',
'PhabricatorPeoplePictureProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeoplePictureProfileMenuItem.php',
'PhabricatorPeopleProfileBadgesController' => 'applications/people/controller/PhabricatorPeopleProfileBadgesController.php',
'PhabricatorPeopleProfileCommitsController' => 'applications/people/controller/PhabricatorPeopleProfileCommitsController.php',
'PhabricatorPeopleProfileController' => 'applications/people/controller/PhabricatorPeopleProfileController.php',
'PhabricatorPeopleProfileEditController' => 'applications/people/controller/PhabricatorPeopleProfileEditController.php',
'PhabricatorPeopleProfileImageWorkflow' => 'applications/people/management/PhabricatorPeopleProfileImageWorkflow.php',
'PhabricatorPeopleProfileManageController' => 'applications/people/controller/PhabricatorPeopleProfileManageController.php',
'PhabricatorPeopleProfileMenuEngine' => 'applications/people/engine/PhabricatorPeopleProfileMenuEngine.php',
'PhabricatorPeopleProfilePictureController' => 'applications/people/controller/PhabricatorPeopleProfilePictureController.php',
'PhabricatorPeopleProfileRevisionsController' => 'applications/people/controller/PhabricatorPeopleProfileRevisionsController.php',
'PhabricatorPeopleProfileTasksController' => 'applications/people/controller/PhabricatorPeopleProfileTasksController.php',
'PhabricatorPeopleProfileViewController' => 'applications/people/controller/PhabricatorPeopleProfileViewController.php',
'PhabricatorPeopleQuery' => 'applications/people/query/PhabricatorPeopleQuery.php',
'PhabricatorPeopleRenameController' => 'applications/people/controller/PhabricatorPeopleRenameController.php',
'PhabricatorPeopleRevisionsProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleRevisionsProfileMenuItem.php',
'PhabricatorPeopleSearchEngine' => 'applications/people/query/PhabricatorPeopleSearchEngine.php',
'PhabricatorPeopleTasksProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleTasksProfileMenuItem.php',
'PhabricatorPeopleTestDataGenerator' => 'applications/people/lipsum/PhabricatorPeopleTestDataGenerator.php',
'PhabricatorPeopleTransactionQuery' => 'applications/people/query/PhabricatorPeopleTransactionQuery.php',
'PhabricatorPeopleUserFunctionDatasource' => 'applications/people/typeahead/PhabricatorPeopleUserFunctionDatasource.php',
'PhabricatorPeopleUserPHIDType' => 'applications/people/phid/PhabricatorPeopleUserPHIDType.php',
+ 'PhabricatorPeopleUsernameMailEngine' => 'applications/people/mail/PhabricatorPeopleUsernameMailEngine.php',
'PhabricatorPeopleWelcomeController' => 'applications/people/controller/PhabricatorPeopleWelcomeController.php',
'PhabricatorPeopleWelcomeMailEngine' => 'applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php',
'PhabricatorPhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorPhabricatorAuthProvider.php',
'PhabricatorPhameApplication' => 'applications/phame/application/PhabricatorPhameApplication.php',
'PhabricatorPhameBlogPHIDType' => 'applications/phame/phid/PhabricatorPhameBlogPHIDType.php',
'PhabricatorPhamePostPHIDType' => 'applications/phame/phid/PhabricatorPhamePostPHIDType.php',
'PhabricatorPhluxApplication' => 'applications/phlux/application/PhabricatorPhluxApplication.php',
'PhabricatorPholioApplication' => 'applications/pholio/application/PhabricatorPholioApplication.php',
'PhabricatorPholioMockTestDataGenerator' => 'applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php',
'PhabricatorPhoneNumber' => 'applications/metamta/message/PhabricatorPhoneNumber.php',
'PhabricatorPhoneNumberTestCase' => 'applications/metamta/message/__tests__/PhabricatorPhoneNumberTestCase.php',
'PhabricatorPhortuneApplication' => 'applications/phortune/application/PhabricatorPhortuneApplication.php',
'PhabricatorPhortuneContentSource' => 'applications/phortune/contentsource/PhabricatorPhortuneContentSource.php',
'PhabricatorPhortuneManagementInvoiceWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php',
'PhabricatorPhortuneManagementWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementWorkflow.php',
'PhabricatorPhortuneTestCase' => 'applications/phortune/__tests__/PhabricatorPhortuneTestCase.php',
'PhabricatorPhragmentApplication' => 'applications/phragment/application/PhabricatorPhragmentApplication.php',
'PhabricatorPhrequentApplication' => 'applications/phrequent/application/PhabricatorPhrequentApplication.php',
'PhabricatorPhrictionApplication' => 'applications/phriction/application/PhabricatorPhrictionApplication.php',
'PhabricatorPhurlApplication' => 'applications/phurl/application/PhabricatorPhurlApplication.php',
'PhabricatorPhurlConfigOptions' => 'applications/config/option/PhabricatorPhurlConfigOptions.php',
'PhabricatorPhurlController' => 'applications/phurl/controller/PhabricatorPhurlController.php',
'PhabricatorPhurlDAO' => 'applications/phurl/storage/PhabricatorPhurlDAO.php',
'PhabricatorPhurlLinkRemarkupRule' => 'applications/phurl/remarkup/PhabricatorPhurlLinkRemarkupRule.php',
'PhabricatorPhurlRemarkupRule' => 'applications/phurl/remarkup/PhabricatorPhurlRemarkupRule.php',
'PhabricatorPhurlSchemaSpec' => 'applications/phurl/storage/PhabricatorPhurlSchemaSpec.php',
'PhabricatorPhurlShortURLController' => 'applications/phurl/controller/PhabricatorPhurlShortURLController.php',
'PhabricatorPhurlShortURLDefaultController' => 'applications/phurl/controller/PhabricatorPhurlShortURLDefaultController.php',
'PhabricatorPhurlURL' => 'applications/phurl/storage/PhabricatorPhurlURL.php',
'PhabricatorPhurlURLAccessController' => 'applications/phurl/controller/PhabricatorPhurlURLAccessController.php',
'PhabricatorPhurlURLAliasTransaction' => 'applications/phurl/xaction/PhabricatorPhurlURLAliasTransaction.php',
'PhabricatorPhurlURLCreateCapability' => 'applications/phurl/capability/PhabricatorPhurlURLCreateCapability.php',
'PhabricatorPhurlURLDatasource' => 'applications/phurl/typeahead/PhabricatorPhurlURLDatasource.php',
'PhabricatorPhurlURLDescriptionTransaction' => 'applications/phurl/xaction/PhabricatorPhurlURLDescriptionTransaction.php',
'PhabricatorPhurlURLEditConduitAPIMethod' => 'applications/phurl/conduit/PhabricatorPhurlURLEditConduitAPIMethod.php',
'PhabricatorPhurlURLEditController' => 'applications/phurl/controller/PhabricatorPhurlURLEditController.php',
'PhabricatorPhurlURLEditEngine' => 'applications/phurl/editor/PhabricatorPhurlURLEditEngine.php',
'PhabricatorPhurlURLEditor' => 'applications/phurl/editor/PhabricatorPhurlURLEditor.php',
'PhabricatorPhurlURLListController' => 'applications/phurl/controller/PhabricatorPhurlURLListController.php',
'PhabricatorPhurlURLLongURLTransaction' => 'applications/phurl/xaction/PhabricatorPhurlURLLongURLTransaction.php',
'PhabricatorPhurlURLMailReceiver' => 'applications/phurl/mail/PhabricatorPhurlURLMailReceiver.php',
'PhabricatorPhurlURLNameNgrams' => 'applications/phurl/storage/PhabricatorPhurlURLNameNgrams.php',
'PhabricatorPhurlURLNameTransaction' => 'applications/phurl/xaction/PhabricatorPhurlURLNameTransaction.php',
'PhabricatorPhurlURLPHIDType' => 'applications/phurl/phid/PhabricatorPhurlURLPHIDType.php',
'PhabricatorPhurlURLQuery' => 'applications/phurl/query/PhabricatorPhurlURLQuery.php',
'PhabricatorPhurlURLReplyHandler' => 'applications/phurl/mail/PhabricatorPhurlURLReplyHandler.php',
'PhabricatorPhurlURLSearchConduitAPIMethod' => 'applications/phurl/conduit/PhabricatorPhurlURLSearchConduitAPIMethod.php',
'PhabricatorPhurlURLSearchEngine' => 'applications/phurl/query/PhabricatorPhurlURLSearchEngine.php',
'PhabricatorPhurlURLTransaction' => 'applications/phurl/storage/PhabricatorPhurlURLTransaction.php',
'PhabricatorPhurlURLTransactionComment' => 'applications/phurl/storage/PhabricatorPhurlURLTransactionComment.php',
'PhabricatorPhurlURLTransactionQuery' => 'applications/phurl/query/PhabricatorPhurlURLTransactionQuery.php',
'PhabricatorPhurlURLTransactionType' => 'applications/phurl/xaction/PhabricatorPhurlURLTransactionType.php',
'PhabricatorPhurlURLViewController' => 'applications/phurl/controller/PhabricatorPhurlURLViewController.php',
'PhabricatorPinnedApplicationsSetting' => 'applications/settings/setting/PhabricatorPinnedApplicationsSetting.php',
'PhabricatorPirateEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorPirateEnglishTranslation.php',
'PhabricatorPlatformSite' => 'aphront/site/PhabricatorPlatformSite.php',
'PhabricatorPointsEditField' => 'applications/transactions/editfield/PhabricatorPointsEditField.php',
'PhabricatorPointsFact' => 'applications/fact/fact/PhabricatorPointsFact.php',
'PhabricatorPolicies' => 'applications/policy/constants/PhabricatorPolicies.php',
'PhabricatorPolicy' => 'applications/policy/storage/PhabricatorPolicy.php',
'PhabricatorPolicyApplication' => 'applications/policy/application/PhabricatorPolicyApplication.php',
'PhabricatorPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorPolicyAwareQuery.php',
'PhabricatorPolicyAwareTestQuery' => 'applications/policy/__tests__/PhabricatorPolicyAwareTestQuery.php',
'PhabricatorPolicyCanEditCapability' => 'applications/policy/capability/PhabricatorPolicyCanEditCapability.php',
'PhabricatorPolicyCanInteractCapability' => 'applications/policy/capability/PhabricatorPolicyCanInteractCapability.php',
'PhabricatorPolicyCanJoinCapability' => 'applications/policy/capability/PhabricatorPolicyCanJoinCapability.php',
'PhabricatorPolicyCanViewCapability' => 'applications/policy/capability/PhabricatorPolicyCanViewCapability.php',
'PhabricatorPolicyCapability' => 'applications/policy/capability/PhabricatorPolicyCapability.php',
'PhabricatorPolicyCapabilityTestCase' => 'applications/policy/capability/__tests__/PhabricatorPolicyCapabilityTestCase.php',
'PhabricatorPolicyCodex' => 'applications/policy/codex/PhabricatorPolicyCodex.php',
'PhabricatorPolicyCodexInterface' => 'applications/policy/codex/PhabricatorPolicyCodexInterface.php',
'PhabricatorPolicyCodexRuleDescription' => 'applications/policy/codex/PhabricatorPolicyCodexRuleDescription.php',
'PhabricatorPolicyConfigOptions' => 'applications/policy/config/PhabricatorPolicyConfigOptions.php',
'PhabricatorPolicyConstants' => 'applications/policy/constants/PhabricatorPolicyConstants.php',
'PhabricatorPolicyController' => 'applications/policy/controller/PhabricatorPolicyController.php',
'PhabricatorPolicyDAO' => 'applications/policy/storage/PhabricatorPolicyDAO.php',
'PhabricatorPolicyDataTestCase' => 'applications/policy/__tests__/PhabricatorPolicyDataTestCase.php',
'PhabricatorPolicyEditController' => 'applications/policy/controller/PhabricatorPolicyEditController.php',
'PhabricatorPolicyEditEngineExtension' => 'applications/policy/editor/PhabricatorPolicyEditEngineExtension.php',
'PhabricatorPolicyEditField' => 'applications/transactions/editfield/PhabricatorPolicyEditField.php',
'PhabricatorPolicyException' => 'applications/policy/exception/PhabricatorPolicyException.php',
'PhabricatorPolicyExplainController' => 'applications/policy/controller/PhabricatorPolicyExplainController.php',
'PhabricatorPolicyFavoritesSetting' => 'applications/settings/setting/PhabricatorPolicyFavoritesSetting.php',
'PhabricatorPolicyFilter' => 'applications/policy/filter/PhabricatorPolicyFilter.php',
'PhabricatorPolicyInterface' => 'applications/policy/interface/PhabricatorPolicyInterface.php',
'PhabricatorPolicyManagementShowWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementShowWorkflow.php',
'PhabricatorPolicyManagementUnlockWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php',
'PhabricatorPolicyManagementWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementWorkflow.php',
'PhabricatorPolicyPHIDTypePolicy' => 'applications/policy/phid/PhabricatorPolicyPHIDTypePolicy.php',
'PhabricatorPolicyQuery' => 'applications/policy/query/PhabricatorPolicyQuery.php',
'PhabricatorPolicyRequestExceptionHandler' => 'aphront/handler/PhabricatorPolicyRequestExceptionHandler.php',
'PhabricatorPolicyRule' => 'applications/policy/rule/PhabricatorPolicyRule.php',
'PhabricatorPolicySearchEngineExtension' => 'applications/policy/engineextension/PhabricatorPolicySearchEngineExtension.php',
'PhabricatorPolicyStrengthConstants' => 'applications/policy/constants/PhabricatorPolicyStrengthConstants.php',
'PhabricatorPolicyTestCase' => 'applications/policy/__tests__/PhabricatorPolicyTestCase.php',
'PhabricatorPolicyTestObject' => 'applications/policy/__tests__/PhabricatorPolicyTestObject.php',
'PhabricatorPolicyType' => 'applications/policy/constants/PhabricatorPolicyType.php',
'PhabricatorPonderApplication' => 'applications/ponder/application/PhabricatorPonderApplication.php',
'PhabricatorProfileMenuEditEngine' => 'applications/search/editor/PhabricatorProfileMenuEditEngine.php',
'PhabricatorProfileMenuEditor' => 'applications/search/editor/PhabricatorProfileMenuEditor.php',
'PhabricatorProfileMenuEngine' => 'applications/search/engine/PhabricatorProfileMenuEngine.php',
'PhabricatorProfileMenuItem' => 'applications/search/menuitem/PhabricatorProfileMenuItem.php',
'PhabricatorProfileMenuItemConfiguration' => 'applications/search/storage/PhabricatorProfileMenuItemConfiguration.php',
'PhabricatorProfileMenuItemConfigurationQuery' => 'applications/search/query/PhabricatorProfileMenuItemConfigurationQuery.php',
'PhabricatorProfileMenuItemConfigurationTransaction' => 'applications/search/storage/PhabricatorProfileMenuItemConfigurationTransaction.php',
'PhabricatorProfileMenuItemConfigurationTransactionQuery' => 'applications/search/query/PhabricatorProfileMenuItemConfigurationTransactionQuery.php',
'PhabricatorProfileMenuItemIconSet' => 'applications/search/menuitem/PhabricatorProfileMenuItemIconSet.php',
'PhabricatorProfileMenuItemPHIDType' => 'applications/search/phidtype/PhabricatorProfileMenuItemPHIDType.php',
'PhabricatorProject' => 'applications/project/storage/PhabricatorProject.php',
'PhabricatorProjectAddHeraldAction' => 'applications/project/herald/PhabricatorProjectAddHeraldAction.php',
'PhabricatorProjectApplication' => 'applications/project/application/PhabricatorProjectApplication.php',
'PhabricatorProjectArchiveController' => 'applications/project/controller/PhabricatorProjectArchiveController.php',
'PhabricatorProjectBoardBackgroundController' => 'applications/project/controller/PhabricatorProjectBoardBackgroundController.php',
'PhabricatorProjectBoardController' => 'applications/project/controller/PhabricatorProjectBoardController.php',
'PhabricatorProjectBoardDisableController' => 'applications/project/controller/PhabricatorProjectBoardDisableController.php',
'PhabricatorProjectBoardImportController' => 'applications/project/controller/PhabricatorProjectBoardImportController.php',
'PhabricatorProjectBoardManageController' => 'applications/project/controller/PhabricatorProjectBoardManageController.php',
'PhabricatorProjectBoardReorderController' => 'applications/project/controller/PhabricatorProjectBoardReorderController.php',
'PhabricatorProjectBoardViewController' => 'applications/project/controller/PhabricatorProjectBoardViewController.php',
'PhabricatorProjectBuiltinsExample' => 'applications/uiexample/examples/PhabricatorProjectBuiltinsExample.php',
'PhabricatorProjectCardView' => 'applications/project/view/PhabricatorProjectCardView.php',
'PhabricatorProjectColorTransaction' => 'applications/project/xaction/PhabricatorProjectColorTransaction.php',
'PhabricatorProjectColorsConfigType' => 'applications/project/config/PhabricatorProjectColorsConfigType.php',
'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php',
+ 'PhabricatorProjectColumnAuthorOrder' => 'applications/project/order/PhabricatorProjectColumnAuthorOrder.php',
+ 'PhabricatorProjectColumnCreatedOrder' => 'applications/project/order/PhabricatorProjectColumnCreatedOrder.php',
'PhabricatorProjectColumnDetailController' => 'applications/project/controller/PhabricatorProjectColumnDetailController.php',
'PhabricatorProjectColumnEditController' => 'applications/project/controller/PhabricatorProjectColumnEditController.php',
+ 'PhabricatorProjectColumnHeader' => 'applications/project/order/PhabricatorProjectColumnHeader.php',
'PhabricatorProjectColumnHideController' => 'applications/project/controller/PhabricatorProjectColumnHideController.php',
+ 'PhabricatorProjectColumnLimitTransaction' => 'applications/project/xaction/column/PhabricatorProjectColumnLimitTransaction.php',
+ 'PhabricatorProjectColumnNameTransaction' => 'applications/project/xaction/column/PhabricatorProjectColumnNameTransaction.php',
+ 'PhabricatorProjectColumnNaturalOrder' => 'applications/project/order/PhabricatorProjectColumnNaturalOrder.php',
+ 'PhabricatorProjectColumnOrder' => 'applications/project/order/PhabricatorProjectColumnOrder.php',
+ 'PhabricatorProjectColumnOwnerOrder' => 'applications/project/order/PhabricatorProjectColumnOwnerOrder.php',
'PhabricatorProjectColumnPHIDType' => 'applications/project/phid/PhabricatorProjectColumnPHIDType.php',
+ 'PhabricatorProjectColumnPointsOrder' => 'applications/project/order/PhabricatorProjectColumnPointsOrder.php',
'PhabricatorProjectColumnPosition' => 'applications/project/storage/PhabricatorProjectColumnPosition.php',
'PhabricatorProjectColumnPositionQuery' => 'applications/project/query/PhabricatorProjectColumnPositionQuery.php',
+ 'PhabricatorProjectColumnPriorityOrder' => 'applications/project/order/PhabricatorProjectColumnPriorityOrder.php',
'PhabricatorProjectColumnQuery' => 'applications/project/query/PhabricatorProjectColumnQuery.php',
+ 'PhabricatorProjectColumnRemoveTriggerController' => 'applications/project/controller/PhabricatorProjectColumnRemoveTriggerController.php',
'PhabricatorProjectColumnSearchEngine' => 'applications/project/query/PhabricatorProjectColumnSearchEngine.php',
+ 'PhabricatorProjectColumnStatusOrder' => 'applications/project/order/PhabricatorProjectColumnStatusOrder.php',
+ 'PhabricatorProjectColumnStatusTransaction' => 'applications/project/xaction/column/PhabricatorProjectColumnStatusTransaction.php',
+ 'PhabricatorProjectColumnTitleOrder' => 'applications/project/order/PhabricatorProjectColumnTitleOrder.php',
'PhabricatorProjectColumnTransaction' => 'applications/project/storage/PhabricatorProjectColumnTransaction.php',
'PhabricatorProjectColumnTransactionEditor' => 'applications/project/editor/PhabricatorProjectColumnTransactionEditor.php',
'PhabricatorProjectColumnTransactionQuery' => 'applications/project/query/PhabricatorProjectColumnTransactionQuery.php',
+ 'PhabricatorProjectColumnTransactionType' => 'applications/project/xaction/column/PhabricatorProjectColumnTransactionType.php',
+ 'PhabricatorProjectColumnTriggerTransaction' => 'applications/project/xaction/column/PhabricatorProjectColumnTriggerTransaction.php',
'PhabricatorProjectConfigOptions' => 'applications/project/config/PhabricatorProjectConfigOptions.php',
'PhabricatorProjectConfiguredCustomField' => 'applications/project/customfield/PhabricatorProjectConfiguredCustomField.php',
'PhabricatorProjectController' => 'applications/project/controller/PhabricatorProjectController.php',
'PhabricatorProjectCoreTestCase' => 'applications/project/__tests__/PhabricatorProjectCoreTestCase.php',
'PhabricatorProjectCoverController' => 'applications/project/controller/PhabricatorProjectCoverController.php',
'PhabricatorProjectCustomField' => 'applications/project/customfield/PhabricatorProjectCustomField.php',
'PhabricatorProjectCustomFieldNumericIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldNumericIndex.php',
'PhabricatorProjectCustomFieldStorage' => 'applications/project/storage/PhabricatorProjectCustomFieldStorage.php',
'PhabricatorProjectCustomFieldStringIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldStringIndex.php',
'PhabricatorProjectDAO' => 'applications/project/storage/PhabricatorProjectDAO.php',
'PhabricatorProjectDatasource' => 'applications/project/typeahead/PhabricatorProjectDatasource.php',
'PhabricatorProjectDefaultController' => 'applications/project/controller/PhabricatorProjectDefaultController.php',
'PhabricatorProjectDescriptionField' => 'applications/project/customfield/PhabricatorProjectDescriptionField.php',
'PhabricatorProjectDetailsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectDetailsProfileMenuItem.php',
+ 'PhabricatorProjectDropEffect' => 'applications/project/icon/PhabricatorProjectDropEffect.php',
'PhabricatorProjectEditController' => 'applications/project/controller/PhabricatorProjectEditController.php',
'PhabricatorProjectEditEngine' => 'applications/project/engine/PhabricatorProjectEditEngine.php',
'PhabricatorProjectEditPictureController' => 'applications/project/controller/PhabricatorProjectEditPictureController.php',
'PhabricatorProjectFerretEngine' => 'applications/project/search/PhabricatorProjectFerretEngine.php',
'PhabricatorProjectFilterTransaction' => 'applications/project/xaction/PhabricatorProjectFilterTransaction.php',
'PhabricatorProjectFulltextEngine' => 'applications/project/search/PhabricatorProjectFulltextEngine.php',
'PhabricatorProjectHeraldAction' => 'applications/project/herald/PhabricatorProjectHeraldAction.php',
'PhabricatorProjectHeraldAdapter' => 'applications/project/herald/PhabricatorProjectHeraldAdapter.php',
'PhabricatorProjectHeraldFieldGroup' => 'applications/project/herald/PhabricatorProjectHeraldFieldGroup.php',
'PhabricatorProjectHovercardEngineExtension' => 'applications/project/engineextension/PhabricatorProjectHovercardEngineExtension.php',
'PhabricatorProjectIconSet' => 'applications/project/icon/PhabricatorProjectIconSet.php',
'PhabricatorProjectIconTransaction' => 'applications/project/xaction/PhabricatorProjectIconTransaction.php',
'PhabricatorProjectIconsConfigType' => 'applications/project/config/PhabricatorProjectIconsConfigType.php',
'PhabricatorProjectImageTransaction' => 'applications/project/xaction/PhabricatorProjectImageTransaction.php',
'PhabricatorProjectInterface' => 'applications/project/interface/PhabricatorProjectInterface.php',
'PhabricatorProjectListController' => 'applications/project/controller/PhabricatorProjectListController.php',
'PhabricatorProjectListView' => 'applications/project/view/PhabricatorProjectListView.php',
'PhabricatorProjectLockController' => 'applications/project/controller/PhabricatorProjectLockController.php',
'PhabricatorProjectLockTransaction' => 'applications/project/xaction/PhabricatorProjectLockTransaction.php',
'PhabricatorProjectLogicalAncestorDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalAncestorDatasource.php',
'PhabricatorProjectLogicalDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalDatasource.php',
'PhabricatorProjectLogicalOnlyDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalOnlyDatasource.php',
'PhabricatorProjectLogicalOrNotDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalOrNotDatasource.php',
'PhabricatorProjectLogicalUserDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php',
'PhabricatorProjectLogicalViewerDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalViewerDatasource.php',
'PhabricatorProjectManageController' => 'applications/project/controller/PhabricatorProjectManageController.php',
'PhabricatorProjectManageProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectManageProfileMenuItem.php',
'PhabricatorProjectMaterializedMemberEdgeType' => 'applications/project/edge/PhabricatorProjectMaterializedMemberEdgeType.php',
'PhabricatorProjectMemberListView' => 'applications/project/view/PhabricatorProjectMemberListView.php',
'PhabricatorProjectMemberOfProjectEdgeType' => 'applications/project/edge/PhabricatorProjectMemberOfProjectEdgeType.php',
'PhabricatorProjectMembersAddController' => 'applications/project/controller/PhabricatorProjectMembersAddController.php',
'PhabricatorProjectMembersDatasource' => 'applications/project/typeahead/PhabricatorProjectMembersDatasource.php',
'PhabricatorProjectMembersPolicyRule' => 'applications/project/policyrule/PhabricatorProjectMembersPolicyRule.php',
'PhabricatorProjectMembersProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectMembersProfileMenuItem.php',
'PhabricatorProjectMembersRemoveController' => 'applications/project/controller/PhabricatorProjectMembersRemoveController.php',
'PhabricatorProjectMembersViewController' => 'applications/project/controller/PhabricatorProjectMembersViewController.php',
'PhabricatorProjectMenuItemController' => 'applications/project/controller/PhabricatorProjectMenuItemController.php',
'PhabricatorProjectMilestoneTransaction' => 'applications/project/xaction/PhabricatorProjectMilestoneTransaction.php',
'PhabricatorProjectMoveController' => 'applications/project/controller/PhabricatorProjectMoveController.php',
'PhabricatorProjectNameContextFreeGrammar' => 'applications/project/lipsum/PhabricatorProjectNameContextFreeGrammar.php',
'PhabricatorProjectNameTransaction' => 'applications/project/xaction/PhabricatorProjectNameTransaction.php',
'PhabricatorProjectNoProjectsDatasource' => 'applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php',
'PhabricatorProjectObjectHasProjectEdgeType' => 'applications/project/edge/PhabricatorProjectObjectHasProjectEdgeType.php',
'PhabricatorProjectOrUserDatasource' => 'applications/project/typeahead/PhabricatorProjectOrUserDatasource.php',
'PhabricatorProjectOrUserFunctionDatasource' => 'applications/project/typeahead/PhabricatorProjectOrUserFunctionDatasource.php',
'PhabricatorProjectPHIDResolver' => 'applications/phid/resolver/PhabricatorProjectPHIDResolver.php',
'PhabricatorProjectParentTransaction' => 'applications/project/xaction/PhabricatorProjectParentTransaction.php',
'PhabricatorProjectPictureProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectPictureProfileMenuItem.php',
'PhabricatorProjectPointsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectPointsProfileMenuItem.php',
'PhabricatorProjectProfileController' => 'applications/project/controller/PhabricatorProjectProfileController.php',
'PhabricatorProjectProfileMenuEngine' => 'applications/project/engine/PhabricatorProjectProfileMenuEngine.php',
'PhabricatorProjectProfileMenuItem' => 'applications/search/menuitem/PhabricatorProjectProfileMenuItem.php',
'PhabricatorProjectProjectHasMemberEdgeType' => 'applications/project/edge/PhabricatorProjectProjectHasMemberEdgeType.php',
'PhabricatorProjectProjectHasObjectEdgeType' => 'applications/project/edge/PhabricatorProjectProjectHasObjectEdgeType.php',
'PhabricatorProjectProjectPHIDType' => 'applications/project/phid/PhabricatorProjectProjectPHIDType.php',
'PhabricatorProjectQuery' => 'applications/project/query/PhabricatorProjectQuery.php',
'PhabricatorProjectRemoveHeraldAction' => 'applications/project/herald/PhabricatorProjectRemoveHeraldAction.php',
'PhabricatorProjectSchemaSpec' => 'applications/project/storage/PhabricatorProjectSchemaSpec.php',
'PhabricatorProjectSearchEngine' => 'applications/project/query/PhabricatorProjectSearchEngine.php',
'PhabricatorProjectSearchField' => 'applications/project/searchfield/PhabricatorProjectSearchField.php',
'PhabricatorProjectSilenceController' => 'applications/project/controller/PhabricatorProjectSilenceController.php',
'PhabricatorProjectSilencedEdgeType' => 'applications/project/edge/PhabricatorProjectSilencedEdgeType.php',
'PhabricatorProjectSlug' => 'applications/project/storage/PhabricatorProjectSlug.php',
'PhabricatorProjectSlugsTransaction' => 'applications/project/xaction/PhabricatorProjectSlugsTransaction.php',
'PhabricatorProjectSortTransaction' => 'applications/project/xaction/PhabricatorProjectSortTransaction.php',
'PhabricatorProjectStandardCustomField' => 'applications/project/customfield/PhabricatorProjectStandardCustomField.php',
'PhabricatorProjectStatus' => 'applications/project/constants/PhabricatorProjectStatus.php',
'PhabricatorProjectStatusTransaction' => 'applications/project/xaction/PhabricatorProjectStatusTransaction.php',
'PhabricatorProjectSubprojectWarningController' => 'applications/project/controller/PhabricatorProjectSubprojectWarningController.php',
'PhabricatorProjectSubprojectsController' => 'applications/project/controller/PhabricatorProjectSubprojectsController.php',
'PhabricatorProjectSubprojectsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectSubprojectsProfileMenuItem.php',
'PhabricatorProjectSubtypeDatasource' => 'applications/project/typeahead/PhabricatorProjectSubtypeDatasource.php',
'PhabricatorProjectSubtypesConfigType' => 'applications/project/config/PhabricatorProjectSubtypesConfigType.php',
'PhabricatorProjectTestDataGenerator' => 'applications/project/lipsum/PhabricatorProjectTestDataGenerator.php',
'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php',
'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php',
'PhabricatorProjectTransactionQuery' => 'applications/project/query/PhabricatorProjectTransactionQuery.php',
'PhabricatorProjectTransactionType' => 'applications/project/xaction/PhabricatorProjectTransactionType.php',
+ 'PhabricatorProjectTrigger' => 'applications/project/storage/PhabricatorProjectTrigger.php',
+ 'PhabricatorProjectTriggerController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerController.php',
+ 'PhabricatorProjectTriggerCorruptionException' => 'applications/project/exception/PhabricatorProjectTriggerCorruptionException.php',
+ 'PhabricatorProjectTriggerEditController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php',
+ 'PhabricatorProjectTriggerEditor' => 'applications/project/editor/PhabricatorProjectTriggerEditor.php',
+ 'PhabricatorProjectTriggerInvalidRule' => 'applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php',
+ 'PhabricatorProjectTriggerListController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerListController.php',
+ 'PhabricatorProjectTriggerManiphestPriorityRule' => 'applications/project/trigger/PhabricatorProjectTriggerManiphestPriorityRule.php',
+ 'PhabricatorProjectTriggerManiphestStatusRule' => 'applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php',
+ 'PhabricatorProjectTriggerNameTransaction' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerNameTransaction.php',
+ 'PhabricatorProjectTriggerPHIDType' => 'applications/project/phid/PhabricatorProjectTriggerPHIDType.php',
+ 'PhabricatorProjectTriggerPlaySoundRule' => 'applications/project/trigger/PhabricatorProjectTriggerPlaySoundRule.php',
+ 'PhabricatorProjectTriggerQuery' => 'applications/project/query/PhabricatorProjectTriggerQuery.php',
+ 'PhabricatorProjectTriggerRule' => 'applications/project/trigger/PhabricatorProjectTriggerRule.php',
+ 'PhabricatorProjectTriggerRuleRecord' => 'applications/project/trigger/PhabricatorProjectTriggerRuleRecord.php',
+ 'PhabricatorProjectTriggerRulesetTransaction' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerRulesetTransaction.php',
+ 'PhabricatorProjectTriggerSearchEngine' => 'applications/project/query/PhabricatorProjectTriggerSearchEngine.php',
+ 'PhabricatorProjectTriggerTransaction' => 'applications/project/storage/PhabricatorProjectTriggerTransaction.php',
+ 'PhabricatorProjectTriggerTransactionQuery' => 'applications/project/query/PhabricatorProjectTriggerTransactionQuery.php',
+ 'PhabricatorProjectTriggerTransactionType' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php',
+ 'PhabricatorProjectTriggerUnknownRule' => 'applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php',
+ 'PhabricatorProjectTriggerUsage' => 'applications/project/storage/PhabricatorProjectTriggerUsage.php',
+ 'PhabricatorProjectTriggerUsageIndexEngineExtension' => 'applications/project/engineextension/PhabricatorProjectTriggerUsageIndexEngineExtension.php',
+ 'PhabricatorProjectTriggerViewController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php',
'PhabricatorProjectTypeTransaction' => 'applications/project/xaction/PhabricatorProjectTypeTransaction.php',
'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php',
'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php',
'PhabricatorProjectUserFunctionDatasource' => 'applications/project/typeahead/PhabricatorProjectUserFunctionDatasource.php',
'PhabricatorProjectUserListView' => 'applications/project/view/PhabricatorProjectUserListView.php',
'PhabricatorProjectViewController' => 'applications/project/controller/PhabricatorProjectViewController.php',
'PhabricatorProjectWatchController' => 'applications/project/controller/PhabricatorProjectWatchController.php',
'PhabricatorProjectWatcherListView' => 'applications/project/view/PhabricatorProjectWatcherListView.php',
'PhabricatorProjectWorkboardBackgroundColor' => 'applications/project/constants/PhabricatorProjectWorkboardBackgroundColor.php',
'PhabricatorProjectWorkboardBackgroundTransaction' => 'applications/project/xaction/PhabricatorProjectWorkboardBackgroundTransaction.php',
'PhabricatorProjectWorkboardProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectWorkboardProfileMenuItem.php',
'PhabricatorProjectWorkboardTransaction' => 'applications/project/xaction/PhabricatorProjectWorkboardTransaction.php',
'PhabricatorProjectsAllPolicyRule' => 'applications/project/policyrule/PhabricatorProjectsAllPolicyRule.php',
'PhabricatorProjectsAncestorsSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsAncestorsSearchEngineAttachment.php',
'PhabricatorProjectsBasePolicyRule' => 'applications/project/policyrule/PhabricatorProjectsBasePolicyRule.php',
'PhabricatorProjectsCurtainExtension' => 'applications/project/engineextension/PhabricatorProjectsCurtainExtension.php',
'PhabricatorProjectsEditEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsEditEngineExtension.php',
'PhabricatorProjectsEditField' => 'applications/transactions/editfield/PhabricatorProjectsEditField.php',
'PhabricatorProjectsExportEngineExtension' => 'infrastructure/export/engine/PhabricatorProjectsExportEngineExtension.php',
'PhabricatorProjectsFulltextEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsFulltextEngineExtension.php',
'PhabricatorProjectsMailEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsMailEngineExtension.php',
'PhabricatorProjectsMembersSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsMembersSearchEngineAttachment.php',
'PhabricatorProjectsMembershipIndexEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsMembershipIndexEngineExtension.php',
'PhabricatorProjectsPolicyRule' => 'applications/project/policyrule/PhabricatorProjectsPolicyRule.php',
'PhabricatorProjectsSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsSearchEngineAttachment.php',
'PhabricatorProjectsSearchEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsSearchEngineExtension.php',
'PhabricatorProjectsWatchersSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsWatchersSearchEngineAttachment.php',
'PhabricatorPronounSetting' => 'applications/settings/setting/PhabricatorPronounSetting.php',
'PhabricatorPygmentSetupCheck' => 'applications/config/check/PhabricatorPygmentSetupCheck.php',
'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php',
'PhabricatorQueryConstraint' => 'infrastructure/query/constraint/PhabricatorQueryConstraint.php',
+ 'PhabricatorQueryCursor' => 'infrastructure/query/policy/PhabricatorQueryCursor.php',
'PhabricatorQueryIterator' => 'infrastructure/storage/lisk/PhabricatorQueryIterator.php',
'PhabricatorQueryOrderItem' => 'infrastructure/query/order/PhabricatorQueryOrderItem.php',
'PhabricatorQueryOrderTestCase' => 'infrastructure/query/order/__tests__/PhabricatorQueryOrderTestCase.php',
'PhabricatorQueryOrderVector' => 'infrastructure/query/order/PhabricatorQueryOrderVector.php',
'PhabricatorQuickSearchEngineExtension' => 'applications/search/engineextension/PhabricatorQuickSearchEngineExtension.php',
'PhabricatorRateLimitRequestExceptionHandler' => 'aphront/handler/PhabricatorRateLimitRequestExceptionHandler.php',
'PhabricatorRecaptchaConfigOptions' => 'applications/config/option/PhabricatorRecaptchaConfigOptions.php',
'PhabricatorRedirectController' => 'applications/base/controller/PhabricatorRedirectController.php',
'PhabricatorRefreshCSRFController' => 'applications/auth/controller/PhabricatorRefreshCSRFController.php',
'PhabricatorRegexListConfigType' => 'applications/config/type/PhabricatorRegexListConfigType.php',
'PhabricatorRegistrationProfile' => 'applications/people/storage/PhabricatorRegistrationProfile.php',
'PhabricatorReleephApplication' => 'applications/releeph/application/PhabricatorReleephApplication.php',
'PhabricatorReleephApplicationConfigOptions' => 'applications/releeph/config/PhabricatorReleephApplicationConfigOptions.php',
'PhabricatorRemarkupCachePurger' => 'applications/cache/purger/PhabricatorRemarkupCachePurger.php',
'PhabricatorRemarkupControl' => 'view/form/control/PhabricatorRemarkupControl.php',
'PhabricatorRemarkupCowsayBlockInterpreter' => 'infrastructure/markup/interpreter/PhabricatorRemarkupCowsayBlockInterpreter.php',
'PhabricatorRemarkupCustomBlockRule' => 'infrastructure/markup/rule/PhabricatorRemarkupCustomBlockRule.php',
'PhabricatorRemarkupCustomInlineRule' => 'infrastructure/markup/rule/PhabricatorRemarkupCustomInlineRule.php',
'PhabricatorRemarkupDocumentEngine' => 'applications/files/document/PhabricatorRemarkupDocumentEngine.php',
'PhabricatorRemarkupEditField' => 'applications/transactions/editfield/PhabricatorRemarkupEditField.php',
'PhabricatorRemarkupFigletBlockInterpreter' => 'infrastructure/markup/interpreter/PhabricatorRemarkupFigletBlockInterpreter.php',
'PhabricatorRemarkupUIExample' => 'applications/uiexample/examples/PhabricatorRemarkupUIExample.php',
'PhabricatorRepositoriesSetupCheck' => 'applications/config/check/PhabricatorRepositoriesSetupCheck.php',
'PhabricatorRepository' => 'applications/repository/storage/PhabricatorRepository.php',
'PhabricatorRepositoryActivateTransaction' => 'applications/repository/xaction/PhabricatorRepositoryActivateTransaction.php',
'PhabricatorRepositoryAuditRequest' => 'applications/repository/storage/PhabricatorRepositoryAuditRequest.php',
'PhabricatorRepositoryAutocloseOnlyTransaction' => 'applications/repository/xaction/PhabricatorRepositoryAutocloseOnlyTransaction.php',
'PhabricatorRepositoryAutocloseTransaction' => 'applications/repository/xaction/PhabricatorRepositoryAutocloseTransaction.php',
'PhabricatorRepositoryBlueprintsTransaction' => 'applications/repository/xaction/PhabricatorRepositoryBlueprintsTransaction.php',
'PhabricatorRepositoryBranch' => 'applications/repository/storage/PhabricatorRepositoryBranch.php',
'PhabricatorRepositoryCallsignTransaction' => 'applications/repository/xaction/PhabricatorRepositoryCallsignTransaction.php',
'PhabricatorRepositoryCommit' => 'applications/repository/storage/PhabricatorRepositoryCommit.php',
'PhabricatorRepositoryCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php',
'PhabricatorRepositoryCommitData' => 'applications/repository/storage/PhabricatorRepositoryCommitData.php',
'PhabricatorRepositoryCommitHeraldWorker' => 'applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php',
'PhabricatorRepositoryCommitHint' => 'applications/repository/storage/PhabricatorRepositoryCommitHint.php',
'PhabricatorRepositoryCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php',
'PhabricatorRepositoryCommitOwnersWorker' => 'applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php',
'PhabricatorRepositoryCommitPHIDType' => 'applications/repository/phid/PhabricatorRepositoryCommitPHIDType.php',
'PhabricatorRepositoryCommitParserWorker' => 'applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php',
'PhabricatorRepositoryCommitRef' => 'applications/repository/engine/PhabricatorRepositoryCommitRef.php',
'PhabricatorRepositoryCommitTestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryCommitTestCase.php',
'PhabricatorRepositoryConfigOptions' => 'applications/repository/config/PhabricatorRepositoryConfigOptions.php',
'PhabricatorRepositoryCopyTimeLimitTransaction' => 'applications/repository/xaction/PhabricatorRepositoryCopyTimeLimitTransaction.php',
'PhabricatorRepositoryDAO' => 'applications/repository/storage/PhabricatorRepositoryDAO.php',
'PhabricatorRepositoryDangerousTransaction' => 'applications/repository/xaction/PhabricatorRepositoryDangerousTransaction.php',
'PhabricatorRepositoryDefaultBranchTransaction' => 'applications/repository/xaction/PhabricatorRepositoryDefaultBranchTransaction.php',
'PhabricatorRepositoryDescriptionTransaction' => 'applications/repository/xaction/PhabricatorRepositoryDescriptionTransaction.php',
'PhabricatorRepositoryDestructibleCodex' => 'applications/repository/codex/PhabricatorRepositoryDestructibleCodex.php',
'PhabricatorRepositoryDiscoveryEngine' => 'applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php',
'PhabricatorRepositoryEditor' => 'applications/repository/editor/PhabricatorRepositoryEditor.php',
'PhabricatorRepositoryEncodingTransaction' => 'applications/repository/xaction/PhabricatorRepositoryEncodingTransaction.php',
'PhabricatorRepositoryEngine' => 'applications/repository/engine/PhabricatorRepositoryEngine.php',
'PhabricatorRepositoryEnormousTransaction' => 'applications/repository/xaction/PhabricatorRepositoryEnormousTransaction.php',
'PhabricatorRepositoryFerretEngine' => 'applications/repository/search/PhabricatorRepositoryFerretEngine.php',
'PhabricatorRepositoryFilesizeLimitTransaction' => 'applications/repository/xaction/PhabricatorRepositoryFilesizeLimitTransaction.php',
'PhabricatorRepositoryFulltextEngine' => 'applications/repository/search/PhabricatorRepositoryFulltextEngine.php',
'PhabricatorRepositoryGitCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php',
'PhabricatorRepositoryGitCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryGitCommitMessageParserWorker.php',
'PhabricatorRepositoryGitLFSRef' => 'applications/repository/storage/PhabricatorRepositoryGitLFSRef.php',
'PhabricatorRepositoryGitLFSRefQuery' => 'applications/repository/query/PhabricatorRepositoryGitLFSRefQuery.php',
'PhabricatorRepositoryGraphCache' => 'applications/repository/graphcache/PhabricatorRepositoryGraphCache.php',
'PhabricatorRepositoryGraphStream' => 'applications/repository/daemon/PhabricatorRepositoryGraphStream.php',
'PhabricatorRepositoryIdentity' => 'applications/repository/storage/PhabricatorRepositoryIdentity.php',
'PhabricatorRepositoryIdentityAssignTransaction' => 'applications/repository/xaction/PhabricatorRepositoryIdentityAssignTransaction.php',
'PhabricatorRepositoryIdentityChangeWorker' => 'applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php',
'PhabricatorRepositoryIdentityEditEngine' => 'applications/repository/engine/PhabricatorRepositoryIdentityEditEngine.php',
'PhabricatorRepositoryIdentityFerretEngine' => 'applications/repository/search/PhabricatorRepositoryIdentityFerretEngine.php',
'PhabricatorRepositoryIdentityPHIDType' => 'applications/repository/phid/PhabricatorRepositoryIdentityPHIDType.php',
'PhabricatorRepositoryIdentityQuery' => 'applications/repository/query/PhabricatorRepositoryIdentityQuery.php',
'PhabricatorRepositoryIdentityTransaction' => 'applications/repository/storage/PhabricatorRepositoryIdentityTransaction.php',
'PhabricatorRepositoryIdentityTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryIdentityTransactionQuery.php',
'PhabricatorRepositoryIdentityTransactionType' => 'applications/repository/xaction/PhabricatorRepositoryIdentityTransactionType.php',
'PhabricatorRepositoryManagementCacheWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementCacheWorkflow.php',
'PhabricatorRepositoryManagementClusterizeWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementClusterizeWorkflow.php',
'PhabricatorRepositoryManagementDiscoverWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php',
'PhabricatorRepositoryManagementHintWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementHintWorkflow.php',
'PhabricatorRepositoryManagementImportingWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementImportingWorkflow.php',
'PhabricatorRepositoryManagementListPathsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementListPathsWorkflow.php',
'PhabricatorRepositoryManagementListWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php',
'PhabricatorRepositoryManagementLookupUsersWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php',
'PhabricatorRepositoryManagementMarkImportedWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMarkImportedWorkflow.php',
'PhabricatorRepositoryManagementMarkReachableWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMarkReachableWorkflow.php',
'PhabricatorRepositoryManagementMirrorWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMirrorWorkflow.php',
'PhabricatorRepositoryManagementMovePathsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMovePathsWorkflow.php',
'PhabricatorRepositoryManagementParentsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementParentsWorkflow.php',
'PhabricatorRepositoryManagementPullWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementPullWorkflow.php',
'PhabricatorRepositoryManagementRebuildIdentitiesWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php',
'PhabricatorRepositoryManagementRefsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementRefsWorkflow.php',
'PhabricatorRepositoryManagementReparseWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementReparseWorkflow.php',
'PhabricatorRepositoryManagementThawWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php',
'PhabricatorRepositoryManagementUnpublishWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementUnpublishWorkflow.php',
'PhabricatorRepositoryManagementUpdateWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementUpdateWorkflow.php',
'PhabricatorRepositoryManagementWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementWorkflow.php',
'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryMercurialCommitChangeParserWorker.php',
'PhabricatorRepositoryMercurialCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryMercurialCommitMessageParserWorker.php',
'PhabricatorRepositoryMirror' => 'applications/repository/storage/PhabricatorRepositoryMirror.php',
'PhabricatorRepositoryMirrorEngine' => 'applications/repository/engine/PhabricatorRepositoryMirrorEngine.php',
'PhabricatorRepositoryNameTransaction' => 'applications/repository/xaction/PhabricatorRepositoryNameTransaction.php',
'PhabricatorRepositoryNotifyTransaction' => 'applications/repository/xaction/PhabricatorRepositoryNotifyTransaction.php',
'PhabricatorRepositoryOldRef' => 'applications/repository/storage/PhabricatorRepositoryOldRef.php',
'PhabricatorRepositoryParsedChange' => 'applications/repository/data/PhabricatorRepositoryParsedChange.php',
'PhabricatorRepositoryPullEngine' => 'applications/repository/engine/PhabricatorRepositoryPullEngine.php',
'PhabricatorRepositoryPullEvent' => 'applications/repository/storage/PhabricatorRepositoryPullEvent.php',
'PhabricatorRepositoryPullEventPHIDType' => 'applications/repository/phid/PhabricatorRepositoryPullEventPHIDType.php',
'PhabricatorRepositoryPullEventQuery' => 'applications/repository/query/PhabricatorRepositoryPullEventQuery.php',
'PhabricatorRepositoryPullLocalDaemon' => 'applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php',
'PhabricatorRepositoryPullLocalDaemonModule' => 'applications/repository/daemon/PhabricatorRepositoryPullLocalDaemonModule.php',
'PhabricatorRepositoryPushEvent' => 'applications/repository/storage/PhabricatorRepositoryPushEvent.php',
'PhabricatorRepositoryPushEventPHIDType' => 'applications/repository/phid/PhabricatorRepositoryPushEventPHIDType.php',
'PhabricatorRepositoryPushEventQuery' => 'applications/repository/query/PhabricatorRepositoryPushEventQuery.php',
'PhabricatorRepositoryPushLog' => 'applications/repository/storage/PhabricatorRepositoryPushLog.php',
'PhabricatorRepositoryPushLogPHIDType' => 'applications/repository/phid/PhabricatorRepositoryPushLogPHIDType.php',
'PhabricatorRepositoryPushLogQuery' => 'applications/repository/query/PhabricatorRepositoryPushLogQuery.php',
'PhabricatorRepositoryPushLogSearchEngine' => 'applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php',
'PhabricatorRepositoryPushMailWorker' => 'applications/repository/worker/PhabricatorRepositoryPushMailWorker.php',
'PhabricatorRepositoryPushPolicyTransaction' => 'applications/repository/xaction/PhabricatorRepositoryPushPolicyTransaction.php',
'PhabricatorRepositoryPushReplyHandler' => 'applications/repository/mail/PhabricatorRepositoryPushReplyHandler.php',
'PhabricatorRepositoryQuery' => 'applications/repository/query/PhabricatorRepositoryQuery.php',
'PhabricatorRepositoryRefCursor' => 'applications/repository/storage/PhabricatorRepositoryRefCursor.php',
'PhabricatorRepositoryRefCursorPHIDType' => 'applications/repository/phid/PhabricatorRepositoryRefCursorPHIDType.php',
'PhabricatorRepositoryRefCursorQuery' => 'applications/repository/query/PhabricatorRepositoryRefCursorQuery.php',
'PhabricatorRepositoryRefEngine' => 'applications/repository/engine/PhabricatorRepositoryRefEngine.php',
'PhabricatorRepositoryRefPosition' => 'applications/repository/storage/PhabricatorRepositoryRefPosition.php',
'PhabricatorRepositoryRepositoryPHIDType' => 'applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php',
'PhabricatorRepositorySVNSubpathTransaction' => 'applications/repository/xaction/PhabricatorRepositorySVNSubpathTransaction.php',
'PhabricatorRepositorySchemaSpec' => 'applications/repository/storage/PhabricatorRepositorySchemaSpec.php',
'PhabricatorRepositorySearchEngine' => 'applications/repository/query/PhabricatorRepositorySearchEngine.php',
'PhabricatorRepositoryServiceTransaction' => 'applications/repository/xaction/PhabricatorRepositoryServiceTransaction.php',
'PhabricatorRepositorySlugTransaction' => 'applications/repository/xaction/PhabricatorRepositorySlugTransaction.php',
'PhabricatorRepositoryStagingURITransaction' => 'applications/repository/xaction/PhabricatorRepositoryStagingURITransaction.php',
'PhabricatorRepositoryStatusMessage' => 'applications/repository/storage/PhabricatorRepositoryStatusMessage.php',
'PhabricatorRepositorySvnCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php',
'PhabricatorRepositorySvnCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositorySvnCommitMessageParserWorker.php',
'PhabricatorRepositorySymbol' => 'applications/repository/storage/PhabricatorRepositorySymbol.php',
'PhabricatorRepositorySymbolLanguagesTransaction' => 'applications/repository/xaction/PhabricatorRepositorySymbolLanguagesTransaction.php',
'PhabricatorRepositorySymbolSourcesTransaction' => 'applications/repository/xaction/PhabricatorRepositorySymbolSourcesTransaction.php',
'PhabricatorRepositorySyncEvent' => 'applications/repository/storage/PhabricatorRepositorySyncEvent.php',
'PhabricatorRepositorySyncEventPHIDType' => 'applications/repository/phid/PhabricatorRepositorySyncEventPHIDType.php',
'PhabricatorRepositorySyncEventQuery' => 'applications/repository/query/PhabricatorRepositorySyncEventQuery.php',
'PhabricatorRepositoryTestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php',
'PhabricatorRepositoryTouchLimitTransaction' => 'applications/repository/xaction/PhabricatorRepositoryTouchLimitTransaction.php',
'PhabricatorRepositoryTrackOnlyTransaction' => 'applications/repository/xaction/PhabricatorRepositoryTrackOnlyTransaction.php',
'PhabricatorRepositoryTransaction' => 'applications/repository/storage/PhabricatorRepositoryTransaction.php',
'PhabricatorRepositoryTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryTransactionQuery.php',
'PhabricatorRepositoryTransactionType' => 'applications/repository/xaction/PhabricatorRepositoryTransactionType.php',
'PhabricatorRepositoryType' => 'applications/repository/constants/PhabricatorRepositoryType.php',
'PhabricatorRepositoryURI' => 'applications/repository/storage/PhabricatorRepositoryURI.php',
'PhabricatorRepositoryURIIndex' => 'applications/repository/storage/PhabricatorRepositoryURIIndex.php',
'PhabricatorRepositoryURINormalizer' => 'applications/repository/data/PhabricatorRepositoryURINormalizer.php',
'PhabricatorRepositoryURINormalizerTestCase' => 'applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php',
'PhabricatorRepositoryURIPHIDType' => 'applications/repository/phid/PhabricatorRepositoryURIPHIDType.php',
'PhabricatorRepositoryURIQuery' => 'applications/repository/query/PhabricatorRepositoryURIQuery.php',
'PhabricatorRepositoryURITestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php',
'PhabricatorRepositoryURITransaction' => 'applications/repository/storage/PhabricatorRepositoryURITransaction.php',
'PhabricatorRepositoryURITransactionQuery' => 'applications/repository/query/PhabricatorRepositoryURITransactionQuery.php',
'PhabricatorRepositoryVCSTransaction' => 'applications/repository/xaction/PhabricatorRepositoryVCSTransaction.php',
'PhabricatorRepositoryWorkingCopyVersion' => 'applications/repository/storage/PhabricatorRepositoryWorkingCopyVersion.php',
'PhabricatorRequestExceptionHandler' => 'aphront/handler/PhabricatorRequestExceptionHandler.php',
'PhabricatorResourceSite' => 'aphront/site/PhabricatorResourceSite.php',
'PhabricatorRobotsController' => 'applications/system/controller/PhabricatorRobotsController.php',
'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php',
'PhabricatorSMSAuthFactor' => 'applications/auth/factor/PhabricatorSMSAuthFactor.php',
'PhabricatorSQLPatchList' => 'infrastructure/storage/patch/PhabricatorSQLPatchList.php',
'PhabricatorSSHKeyGenerator' => 'infrastructure/util/PhabricatorSSHKeyGenerator.php',
'PhabricatorSSHKeysSettingsPanel' => 'applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php',
'PhabricatorSSHLog' => 'infrastructure/log/PhabricatorSSHLog.php',
'PhabricatorSSHPassthruCommand' => 'infrastructure/ssh/PhabricatorSSHPassthruCommand.php',
'PhabricatorSSHPublicKeyInterface' => 'applications/auth/sshkey/PhabricatorSSHPublicKeyInterface.php',
'PhabricatorSSHWorkflow' => 'infrastructure/ssh/PhabricatorSSHWorkflow.php',
'PhabricatorSavedQuery' => 'applications/search/storage/PhabricatorSavedQuery.php',
'PhabricatorSavedQueryQuery' => 'applications/search/query/PhabricatorSavedQueryQuery.php',
'PhabricatorScheduleTaskTriggerAction' => 'infrastructure/daemon/workers/action/PhabricatorScheduleTaskTriggerAction.php',
'PhabricatorScopedEnv' => 'infrastructure/env/PhabricatorScopedEnv.php',
'PhabricatorSearchAbstractDocument' => 'applications/search/index/PhabricatorSearchAbstractDocument.php',
'PhabricatorSearchApplication' => 'applications/search/application/PhabricatorSearchApplication.php',
'PhabricatorSearchApplicationSearchEngine' => 'applications/search/query/PhabricatorSearchApplicationSearchEngine.php',
'PhabricatorSearchApplicationStorageEnginePanel' => 'applications/search/applicationpanel/PhabricatorSearchApplicationStorageEnginePanel.php',
'PhabricatorSearchBaseController' => 'applications/search/controller/PhabricatorSearchBaseController.php',
'PhabricatorSearchCheckboxesField' => 'applications/search/field/PhabricatorSearchCheckboxesField.php',
'PhabricatorSearchConstraintException' => 'applications/search/exception/PhabricatorSearchConstraintException.php',
'PhabricatorSearchController' => 'applications/search/controller/PhabricatorSearchController.php',
'PhabricatorSearchCustomFieldProxyField' => 'applications/search/field/PhabricatorSearchCustomFieldProxyField.php',
'PhabricatorSearchDAO' => 'applications/search/storage/PhabricatorSearchDAO.php',
'PhabricatorSearchDatasource' => 'applications/search/typeahead/PhabricatorSearchDatasource.php',
'PhabricatorSearchDatasourceField' => 'applications/search/field/PhabricatorSearchDatasourceField.php',
'PhabricatorSearchDateControlField' => 'applications/search/field/PhabricatorSearchDateControlField.php',
'PhabricatorSearchDateField' => 'applications/search/field/PhabricatorSearchDateField.php',
'PhabricatorSearchDefaultController' => 'applications/search/controller/PhabricatorSearchDefaultController.php',
'PhabricatorSearchDeleteController' => 'applications/search/controller/PhabricatorSearchDeleteController.php',
'PhabricatorSearchDocument' => 'applications/search/storage/document/PhabricatorSearchDocument.php',
'PhabricatorSearchDocumentField' => 'applications/search/storage/document/PhabricatorSearchDocumentField.php',
'PhabricatorSearchDocumentFieldType' => 'applications/search/constants/PhabricatorSearchDocumentFieldType.php',
'PhabricatorSearchDocumentQuery' => 'applications/search/query/PhabricatorSearchDocumentQuery.php',
'PhabricatorSearchDocumentRelationship' => 'applications/search/storage/document/PhabricatorSearchDocumentRelationship.php',
'PhabricatorSearchDocumentTypeDatasource' => 'applications/search/typeahead/PhabricatorSearchDocumentTypeDatasource.php',
'PhabricatorSearchEditController' => 'applications/search/controller/PhabricatorSearchEditController.php',
'PhabricatorSearchEngineAPIMethod' => 'applications/search/engine/PhabricatorSearchEngineAPIMethod.php',
'PhabricatorSearchEngineAttachment' => 'applications/search/engineextension/PhabricatorSearchEngineAttachment.php',
'PhabricatorSearchEngineExtension' => 'applications/search/engineextension/PhabricatorSearchEngineExtension.php',
'PhabricatorSearchEngineExtensionModule' => 'applications/search/engineextension/PhabricatorSearchEngineExtensionModule.php',
'PhabricatorSearchFerretNgramGarbageCollector' => 'applications/search/garbagecollector/PhabricatorSearchFerretNgramGarbageCollector.php',
'PhabricatorSearchField' => 'applications/search/field/PhabricatorSearchField.php',
'PhabricatorSearchHost' => 'infrastructure/cluster/search/PhabricatorSearchHost.php',
'PhabricatorSearchHovercardController' => 'applications/search/controller/PhabricatorSearchHovercardController.php',
'PhabricatorSearchIndexVersion' => 'applications/search/storage/PhabricatorSearchIndexVersion.php',
'PhabricatorSearchIndexVersionDestructionEngineExtension' => 'applications/search/engineextension/PhabricatorSearchIndexVersionDestructionEngineExtension.php',
'PhabricatorSearchManagementIndexWorkflow' => 'applications/search/management/PhabricatorSearchManagementIndexWorkflow.php',
'PhabricatorSearchManagementInitWorkflow' => 'applications/search/management/PhabricatorSearchManagementInitWorkflow.php',
'PhabricatorSearchManagementNgramsWorkflow' => 'applications/search/management/PhabricatorSearchManagementNgramsWorkflow.php',
'PhabricatorSearchManagementQueryWorkflow' => 'applications/search/management/PhabricatorSearchManagementQueryWorkflow.php',
'PhabricatorSearchManagementWorkflow' => 'applications/search/management/PhabricatorSearchManagementWorkflow.php',
'PhabricatorSearchNgrams' => 'applications/search/ngrams/PhabricatorSearchNgrams.php',
'PhabricatorSearchNgramsDestructionEngineExtension' => 'applications/search/engineextension/PhabricatorSearchNgramsDestructionEngineExtension.php',
'PhabricatorSearchOrderController' => 'applications/search/controller/PhabricatorSearchOrderController.php',
'PhabricatorSearchOrderField' => 'applications/search/field/PhabricatorSearchOrderField.php',
'PhabricatorSearchRelationship' => 'applications/search/constants/PhabricatorSearchRelationship.php',
'PhabricatorSearchRelationshipController' => 'applications/search/controller/PhabricatorSearchRelationshipController.php',
'PhabricatorSearchRelationshipSourceController' => 'applications/search/controller/PhabricatorSearchRelationshipSourceController.php',
'PhabricatorSearchResultBucket' => 'applications/search/buckets/PhabricatorSearchResultBucket.php',
'PhabricatorSearchResultBucketGroup' => 'applications/search/buckets/PhabricatorSearchResultBucketGroup.php',
'PhabricatorSearchResultView' => 'applications/search/view/PhabricatorSearchResultView.php',
'PhabricatorSearchSchemaSpec' => 'applications/search/storage/PhabricatorSearchSchemaSpec.php',
'PhabricatorSearchScopeSetting' => 'applications/settings/setting/PhabricatorSearchScopeSetting.php',
'PhabricatorSearchSelectField' => 'applications/search/field/PhabricatorSearchSelectField.php',
'PhabricatorSearchService' => 'infrastructure/cluster/search/PhabricatorSearchService.php',
'PhabricatorSearchStringListField' => 'applications/search/field/PhabricatorSearchStringListField.php',
'PhabricatorSearchSubscribersField' => 'applications/search/field/PhabricatorSearchSubscribersField.php',
'PhabricatorSearchTextField' => 'applications/search/field/PhabricatorSearchTextField.php',
'PhabricatorSearchThreeStateField' => 'applications/search/field/PhabricatorSearchThreeStateField.php',
'PhabricatorSearchTokenizerField' => 'applications/search/field/PhabricatorSearchTokenizerField.php',
'PhabricatorSearchWorker' => 'applications/search/worker/PhabricatorSearchWorker.php',
'PhabricatorSecurityConfigOptions' => 'applications/config/option/PhabricatorSecurityConfigOptions.php',
'PhabricatorSecuritySetupCheck' => 'applications/config/check/PhabricatorSecuritySetupCheck.php',
'PhabricatorSelectEditField' => 'applications/transactions/editfield/PhabricatorSelectEditField.php',
'PhabricatorSelectSetting' => 'applications/settings/setting/PhabricatorSelectSetting.php',
'PhabricatorSessionsSettingsPanel' => 'applications/settings/panel/PhabricatorSessionsSettingsPanel.php',
'PhabricatorSetConfigType' => 'applications/config/type/PhabricatorSetConfigType.php',
'PhabricatorSetting' => 'applications/settings/setting/PhabricatorSetting.php',
'PhabricatorSettingsAccountPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsAccountPanelGroup.php',
'PhabricatorSettingsAddEmailAction' => 'applications/settings/action/PhabricatorSettingsAddEmailAction.php',
'PhabricatorSettingsAdjustController' => 'applications/settings/controller/PhabricatorSettingsAdjustController.php',
'PhabricatorSettingsApplication' => 'applications/settings/application/PhabricatorSettingsApplication.php',
'PhabricatorSettingsApplicationsPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsApplicationsPanelGroup.php',
'PhabricatorSettingsAuthenticationPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsAuthenticationPanelGroup.php',
'PhabricatorSettingsDeveloperPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsDeveloperPanelGroup.php',
'PhabricatorSettingsEditEngine' => 'applications/settings/editor/PhabricatorSettingsEditEngine.php',
'PhabricatorSettingsEmailPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsEmailPanelGroup.php',
'PhabricatorSettingsIssueController' => 'applications/settings/controller/PhabricatorSettingsIssueController.php',
'PhabricatorSettingsListController' => 'applications/settings/controller/PhabricatorSettingsListController.php',
'PhabricatorSettingsLogsPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsLogsPanelGroup.php',
'PhabricatorSettingsMainController' => 'applications/settings/controller/PhabricatorSettingsMainController.php',
'PhabricatorSettingsPanel' => 'applications/settings/panel/PhabricatorSettingsPanel.php',
'PhabricatorSettingsPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsPanelGroup.php',
'PhabricatorSettingsTimezoneController' => 'applications/settings/controller/PhabricatorSettingsTimezoneController.php',
'PhabricatorSetupCheck' => 'applications/config/check/PhabricatorSetupCheck.php',
'PhabricatorSetupCheckTestCase' => 'applications/config/check/__tests__/PhabricatorSetupCheckTestCase.php',
'PhabricatorSetupEngine' => 'applications/config/engine/PhabricatorSetupEngine.php',
'PhabricatorSetupIssue' => 'applications/config/issue/PhabricatorSetupIssue.php',
'PhabricatorSetupIssueUIExample' => 'applications/uiexample/examples/PhabricatorSetupIssueUIExample.php',
'PhabricatorSetupIssueView' => 'applications/config/view/PhabricatorSetupIssueView.php',
'PhabricatorShortSite' => 'aphront/site/PhabricatorShortSite.php',
'PhabricatorShowFiletreeSetting' => 'applications/settings/setting/PhabricatorShowFiletreeSetting.php',
'PhabricatorSimpleEditType' => 'applications/transactions/edittype/PhabricatorSimpleEditType.php',
'PhabricatorSite' => 'aphront/site/PhabricatorSite.php',
'PhabricatorSlackAuthProvider' => 'applications/auth/provider/PhabricatorSlackAuthProvider.php',
'PhabricatorSlowvoteApplication' => 'applications/slowvote/application/PhabricatorSlowvoteApplication.php',
'PhabricatorSlowvoteChoice' => 'applications/slowvote/storage/PhabricatorSlowvoteChoice.php',
'PhabricatorSlowvoteCloseController' => 'applications/slowvote/controller/PhabricatorSlowvoteCloseController.php',
'PhabricatorSlowvoteCloseTransaction' => 'applications/slowvote/xaction/PhabricatorSlowvoteCloseTransaction.php',
'PhabricatorSlowvoteCommentController' => 'applications/slowvote/controller/PhabricatorSlowvoteCommentController.php',
'PhabricatorSlowvoteController' => 'applications/slowvote/controller/PhabricatorSlowvoteController.php',
'PhabricatorSlowvoteDAO' => 'applications/slowvote/storage/PhabricatorSlowvoteDAO.php',
'PhabricatorSlowvoteDefaultViewCapability' => 'applications/slowvote/capability/PhabricatorSlowvoteDefaultViewCapability.php',
'PhabricatorSlowvoteDescriptionTransaction' => 'applications/slowvote/xaction/PhabricatorSlowvoteDescriptionTransaction.php',
'PhabricatorSlowvoteEditController' => 'applications/slowvote/controller/PhabricatorSlowvoteEditController.php',
'PhabricatorSlowvoteEditor' => 'applications/slowvote/editor/PhabricatorSlowvoteEditor.php',
'PhabricatorSlowvoteListController' => 'applications/slowvote/controller/PhabricatorSlowvoteListController.php',
'PhabricatorSlowvoteMailReceiver' => 'applications/slowvote/mail/PhabricatorSlowvoteMailReceiver.php',
'PhabricatorSlowvoteOption' => 'applications/slowvote/storage/PhabricatorSlowvoteOption.php',
'PhabricatorSlowvotePoll' => 'applications/slowvote/storage/PhabricatorSlowvotePoll.php',
'PhabricatorSlowvotePollController' => 'applications/slowvote/controller/PhabricatorSlowvotePollController.php',
'PhabricatorSlowvotePollPHIDType' => 'applications/slowvote/phid/PhabricatorSlowvotePollPHIDType.php',
'PhabricatorSlowvoteQuery' => 'applications/slowvote/query/PhabricatorSlowvoteQuery.php',
'PhabricatorSlowvoteQuestionTransaction' => 'applications/slowvote/xaction/PhabricatorSlowvoteQuestionTransaction.php',
'PhabricatorSlowvoteReplyHandler' => 'applications/slowvote/mail/PhabricatorSlowvoteReplyHandler.php',
'PhabricatorSlowvoteResponsesTransaction' => 'applications/slowvote/xaction/PhabricatorSlowvoteResponsesTransaction.php',
'PhabricatorSlowvoteSchemaSpec' => 'applications/slowvote/storage/PhabricatorSlowvoteSchemaSpec.php',
'PhabricatorSlowvoteSearchEngine' => 'applications/slowvote/query/PhabricatorSlowvoteSearchEngine.php',
'PhabricatorSlowvoteShuffleTransaction' => 'applications/slowvote/xaction/PhabricatorSlowvoteShuffleTransaction.php',
'PhabricatorSlowvoteTransaction' => 'applications/slowvote/storage/PhabricatorSlowvoteTransaction.php',
'PhabricatorSlowvoteTransactionComment' => 'applications/slowvote/storage/PhabricatorSlowvoteTransactionComment.php',
'PhabricatorSlowvoteTransactionQuery' => 'applications/slowvote/query/PhabricatorSlowvoteTransactionQuery.php',
'PhabricatorSlowvoteTransactionType' => 'applications/slowvote/xaction/PhabricatorSlowvoteTransactionType.php',
'PhabricatorSlowvoteVoteController' => 'applications/slowvote/controller/PhabricatorSlowvoteVoteController.php',
'PhabricatorSlug' => 'infrastructure/util/PhabricatorSlug.php',
'PhabricatorSlugTestCase' => 'infrastructure/util/__tests__/PhabricatorSlugTestCase.php',
'PhabricatorSourceCodeView' => 'view/layout/PhabricatorSourceCodeView.php',
'PhabricatorSourceDocumentEngine' => 'applications/files/document/PhabricatorSourceDocumentEngine.php',
'PhabricatorSpaceEditField' => 'applications/transactions/editfield/PhabricatorSpaceEditField.php',
'PhabricatorSpacesApplication' => 'applications/spaces/application/PhabricatorSpacesApplication.php',
'PhabricatorSpacesArchiveController' => 'applications/spaces/controller/PhabricatorSpacesArchiveController.php',
'PhabricatorSpacesCapabilityCreateSpaces' => 'applications/spaces/capability/PhabricatorSpacesCapabilityCreateSpaces.php',
'PhabricatorSpacesCapabilityDefaultEdit' => 'applications/spaces/capability/PhabricatorSpacesCapabilityDefaultEdit.php',
'PhabricatorSpacesCapabilityDefaultView' => 'applications/spaces/capability/PhabricatorSpacesCapabilityDefaultView.php',
'PhabricatorSpacesController' => 'applications/spaces/controller/PhabricatorSpacesController.php',
'PhabricatorSpacesDAO' => 'applications/spaces/storage/PhabricatorSpacesDAO.php',
'PhabricatorSpacesEditController' => 'applications/spaces/controller/PhabricatorSpacesEditController.php',
'PhabricatorSpacesExportEngineExtension' => 'infrastructure/export/engine/PhabricatorSpacesExportEngineExtension.php',
'PhabricatorSpacesInterface' => 'applications/spaces/interface/PhabricatorSpacesInterface.php',
'PhabricatorSpacesListController' => 'applications/spaces/controller/PhabricatorSpacesListController.php',
'PhabricatorSpacesMailEngineExtension' => 'applications/spaces/engineextension/PhabricatorSpacesMailEngineExtension.php',
'PhabricatorSpacesNamespace' => 'applications/spaces/storage/PhabricatorSpacesNamespace.php',
'PhabricatorSpacesNamespaceArchiveTransaction' => 'applications/spaces/xaction/PhabricatorSpacesNamespaceArchiveTransaction.php',
'PhabricatorSpacesNamespaceDatasource' => 'applications/spaces/typeahead/PhabricatorSpacesNamespaceDatasource.php',
'PhabricatorSpacesNamespaceDefaultTransaction' => 'applications/spaces/xaction/PhabricatorSpacesNamespaceDefaultTransaction.php',
'PhabricatorSpacesNamespaceDescriptionTransaction' => 'applications/spaces/xaction/PhabricatorSpacesNamespaceDescriptionTransaction.php',
'PhabricatorSpacesNamespaceEditor' => 'applications/spaces/editor/PhabricatorSpacesNamespaceEditor.php',
'PhabricatorSpacesNamespaceNameTransaction' => 'applications/spaces/xaction/PhabricatorSpacesNamespaceNameTransaction.php',
'PhabricatorSpacesNamespacePHIDType' => 'applications/spaces/phid/PhabricatorSpacesNamespacePHIDType.php',
'PhabricatorSpacesNamespaceQuery' => 'applications/spaces/query/PhabricatorSpacesNamespaceQuery.php',
'PhabricatorSpacesNamespaceSearchEngine' => 'applications/spaces/query/PhabricatorSpacesNamespaceSearchEngine.php',
'PhabricatorSpacesNamespaceTransaction' => 'applications/spaces/storage/PhabricatorSpacesNamespaceTransaction.php',
'PhabricatorSpacesNamespaceTransactionQuery' => 'applications/spaces/query/PhabricatorSpacesNamespaceTransactionQuery.php',
'PhabricatorSpacesNamespaceTransactionType' => 'applications/spaces/xaction/PhabricatorSpacesNamespaceTransactionType.php',
'PhabricatorSpacesNoAccessController' => 'applications/spaces/controller/PhabricatorSpacesNoAccessController.php',
'PhabricatorSpacesRemarkupRule' => 'applications/spaces/remarkup/PhabricatorSpacesRemarkupRule.php',
'PhabricatorSpacesSchemaSpec' => 'applications/spaces/storage/PhabricatorSpacesSchemaSpec.php',
'PhabricatorSpacesSearchEngineExtension' => 'applications/spaces/engineextension/PhabricatorSpacesSearchEngineExtension.php',
'PhabricatorSpacesSearchField' => 'applications/spaces/searchfield/PhabricatorSpacesSearchField.php',
'PhabricatorSpacesTestCase' => 'applications/spaces/__tests__/PhabricatorSpacesTestCase.php',
'PhabricatorSpacesViewController' => 'applications/spaces/controller/PhabricatorSpacesViewController.php',
'PhabricatorStandardCustomField' => 'infrastructure/customfield/standard/PhabricatorStandardCustomField.php',
'PhabricatorStandardCustomFieldBlueprints' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldBlueprints.php',
'PhabricatorStandardCustomFieldBool' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php',
'PhabricatorStandardCustomFieldCredential' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.php',
'PhabricatorStandardCustomFieldDatasource' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldDatasource.php',
'PhabricatorStandardCustomFieldDate' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php',
'PhabricatorStandardCustomFieldHeader' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldHeader.php',
'PhabricatorStandardCustomFieldInt' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php',
'PhabricatorStandardCustomFieldInterface' => 'infrastructure/customfield/interface/PhabricatorStandardCustomFieldInterface.php',
'PhabricatorStandardCustomFieldLink' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php',
'PhabricatorStandardCustomFieldPHIDs' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php',
'PhabricatorStandardCustomFieldRemarkup' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php',
'PhabricatorStandardCustomFieldSelect' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php',
'PhabricatorStandardCustomFieldText' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php',
'PhabricatorStandardCustomFieldTokenizer' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php',
'PhabricatorStandardCustomFieldUsers' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php',
'PhabricatorStandardPageView' => 'view/page/PhabricatorStandardPageView.php',
'PhabricatorStandardSelectCustomFieldDatasource' => 'infrastructure/customfield/datasource/PhabricatorStandardSelectCustomFieldDatasource.php',
'PhabricatorStandardTimelineEngine' => 'applications/transactions/engine/PhabricatorStandardTimelineEngine.php',
'PhabricatorStaticEditField' => 'applications/transactions/editfield/PhabricatorStaticEditField.php',
'PhabricatorStatusController' => 'applications/system/controller/PhabricatorStatusController.php',
'PhabricatorStatusUIExample' => 'applications/uiexample/examples/PhabricatorStatusUIExample.php',
'PhabricatorStorageFixtureScopeGuard' => 'infrastructure/testing/fixture/PhabricatorStorageFixtureScopeGuard.php',
'PhabricatorStorageManagementAPI' => 'infrastructure/storage/management/PhabricatorStorageManagementAPI.php',
'PhabricatorStorageManagementAdjustWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementAdjustWorkflow.php',
'PhabricatorStorageManagementAnalyzeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementAnalyzeWorkflow.php',
'PhabricatorStorageManagementDatabasesWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php',
'PhabricatorStorageManagementDestroyWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php',
'PhabricatorStorageManagementDumpWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php',
'PhabricatorStorageManagementOptimizeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementOptimizeWorkflow.php',
'PhabricatorStorageManagementPartitionWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementPartitionWorkflow.php',
'PhabricatorStorageManagementProbeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementProbeWorkflow.php',
'PhabricatorStorageManagementQuickstartWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementQuickstartWorkflow.php',
'PhabricatorStorageManagementRenamespaceWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementRenamespaceWorkflow.php',
'PhabricatorStorageManagementShellWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php',
'PhabricatorStorageManagementStatusWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementStatusWorkflow.php',
'PhabricatorStorageManagementUpgradeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php',
'PhabricatorStorageManagementWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php',
'PhabricatorStoragePatch' => 'infrastructure/storage/management/PhabricatorStoragePatch.php',
'PhabricatorStorageSchemaSpec' => 'infrastructure/storage/schema/PhabricatorStorageSchemaSpec.php',
'PhabricatorStorageSetupCheck' => 'applications/config/check/PhabricatorStorageSetupCheck.php',
'PhabricatorStringConfigType' => 'applications/config/type/PhabricatorStringConfigType.php',
'PhabricatorStringExportField' => 'infrastructure/export/field/PhabricatorStringExportField.php',
'PhabricatorStringListConfigType' => 'applications/config/type/PhabricatorStringListConfigType.php',
'PhabricatorStringListEditField' => 'applications/transactions/editfield/PhabricatorStringListEditField.php',
'PhabricatorStringListExportField' => 'infrastructure/export/field/PhabricatorStringListExportField.php',
'PhabricatorStringMailStamp' => 'applications/metamta/stamp/PhabricatorStringMailStamp.php',
'PhabricatorStringSetting' => 'applications/settings/setting/PhabricatorStringSetting.php',
'PhabricatorSubmitEditField' => 'applications/transactions/editfield/PhabricatorSubmitEditField.php',
'PhabricatorSubscribableInterface' => 'applications/subscriptions/interface/PhabricatorSubscribableInterface.php',
'PhabricatorSubscribedToObjectEdgeType' => 'applications/transactions/edges/PhabricatorSubscribedToObjectEdgeType.php',
'PhabricatorSubscribersEditField' => 'applications/transactions/editfield/PhabricatorSubscribersEditField.php',
'PhabricatorSubscribersQuery' => 'applications/subscriptions/query/PhabricatorSubscribersQuery.php',
'PhabricatorSubscriptionTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorSubscriptionTriggerClock.php',
'PhabricatorSubscriptionsAddSelfHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsAddSelfHeraldAction.php',
'PhabricatorSubscriptionsAddSubscribersHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsAddSubscribersHeraldAction.php',
'PhabricatorSubscriptionsApplication' => 'applications/subscriptions/application/PhabricatorSubscriptionsApplication.php',
'PhabricatorSubscriptionsCurtainExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsCurtainExtension.php',
'PhabricatorSubscriptionsEditController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php',
'PhabricatorSubscriptionsEditEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsEditEngineExtension.php',
'PhabricatorSubscriptionsEditor' => 'applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php',
'PhabricatorSubscriptionsExportEngineExtension' => 'infrastructure/export/engine/PhabricatorSubscriptionsExportEngineExtension.php',
'PhabricatorSubscriptionsFulltextEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsFulltextEngineExtension.php',
'PhabricatorSubscriptionsHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsHeraldAction.php',
'PhabricatorSubscriptionsListController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsListController.php',
'PhabricatorSubscriptionsMailEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsMailEngineExtension.php',
'PhabricatorSubscriptionsMuteController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php',
'PhabricatorSubscriptionsRemoveSelfHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsRemoveSelfHeraldAction.php',
'PhabricatorSubscriptionsRemoveSubscribersHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsRemoveSubscribersHeraldAction.php',
'PhabricatorSubscriptionsSearchEngineAttachment' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsSearchEngineAttachment.php',
'PhabricatorSubscriptionsSearchEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsSearchEngineExtension.php',
'PhabricatorSubscriptionsSubscribeEmailCommand' => 'applications/subscriptions/command/PhabricatorSubscriptionsSubscribeEmailCommand.php',
'PhabricatorSubscriptionsSubscribersPolicyRule' => 'applications/subscriptions/policyrule/PhabricatorSubscriptionsSubscribersPolicyRule.php',
'PhabricatorSubscriptionsTransactionController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsTransactionController.php',
'PhabricatorSubscriptionsUIEventListener' => 'applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php',
'PhabricatorSubscriptionsUnsubscribeEmailCommand' => 'applications/subscriptions/command/PhabricatorSubscriptionsUnsubscribeEmailCommand.php',
'PhabricatorSubtypeEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php',
'PhabricatorSupportApplication' => 'applications/support/application/PhabricatorSupportApplication.php',
'PhabricatorSyntaxHighlighter' => 'infrastructure/markup/PhabricatorSyntaxHighlighter.php',
'PhabricatorSyntaxHighlightingConfigOptions' => 'applications/config/option/PhabricatorSyntaxHighlightingConfigOptions.php',
'PhabricatorSyntaxStyle' => 'infrastructure/syntax/PhabricatorSyntaxStyle.php',
'PhabricatorSystemAction' => 'applications/system/action/PhabricatorSystemAction.php',
'PhabricatorSystemActionEngine' => 'applications/system/engine/PhabricatorSystemActionEngine.php',
'PhabricatorSystemActionGarbageCollector' => 'applications/system/garbagecollector/PhabricatorSystemActionGarbageCollector.php',
'PhabricatorSystemActionLog' => 'applications/system/storage/PhabricatorSystemActionLog.php',
'PhabricatorSystemActionRateLimitException' => 'applications/system/exception/PhabricatorSystemActionRateLimitException.php',
'PhabricatorSystemApplication' => 'applications/system/application/PhabricatorSystemApplication.php',
'PhabricatorSystemDAO' => 'applications/system/storage/PhabricatorSystemDAO.php',
'PhabricatorSystemDestructionGarbageCollector' => 'applications/system/garbagecollector/PhabricatorSystemDestructionGarbageCollector.php',
'PhabricatorSystemDestructionLog' => 'applications/system/storage/PhabricatorSystemDestructionLog.php',
'PhabricatorSystemObjectController' => 'applications/system/controller/PhabricatorSystemObjectController.php',
'PhabricatorSystemReadOnlyController' => 'applications/system/controller/PhabricatorSystemReadOnlyController.php',
'PhabricatorSystemRemoveDestroyWorkflow' => 'applications/system/management/PhabricatorSystemRemoveDestroyWorkflow.php',
'PhabricatorSystemRemoveLogWorkflow' => 'applications/system/management/PhabricatorSystemRemoveLogWorkflow.php',
'PhabricatorSystemRemoveWorkflow' => 'applications/system/management/PhabricatorSystemRemoveWorkflow.php',
'PhabricatorSystemSelectEncodingController' => 'applications/system/controller/PhabricatorSystemSelectEncodingController.php',
'PhabricatorSystemSelectHighlightController' => 'applications/system/controller/PhabricatorSystemSelectHighlightController.php',
'PhabricatorTOTPAuthFactor' => 'applications/auth/factor/PhabricatorTOTPAuthFactor.php',
'PhabricatorTOTPAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorTOTPAuthFactorTestCase.php',
'PhabricatorTaskmasterDaemon' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php',
'PhabricatorTaskmasterDaemonModule' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemonModule.php',
'PhabricatorTestApplication' => 'applications/base/controller/__tests__/PhabricatorTestApplication.php',
'PhabricatorTestCase' => 'infrastructure/testing/PhabricatorTestCase.php',
'PhabricatorTestController' => 'applications/base/controller/__tests__/PhabricatorTestController.php',
'PhabricatorTestDataGenerator' => 'applications/lipsum/generator/PhabricatorTestDataGenerator.php',
'PhabricatorTestNoCycleEdgeType' => 'applications/transactions/edges/PhabricatorTestNoCycleEdgeType.php',
'PhabricatorTestStorageEngine' => 'applications/files/engine/PhabricatorTestStorageEngine.php',
'PhabricatorTestWorker' => 'infrastructure/daemon/workers/__tests__/PhabricatorTestWorker.php',
'PhabricatorTextAreaEditField' => 'applications/transactions/editfield/PhabricatorTextAreaEditField.php',
'PhabricatorTextConfigType' => 'applications/config/type/PhabricatorTextConfigType.php',
'PhabricatorTextDocumentEngine' => 'applications/files/document/PhabricatorTextDocumentEngine.php',
'PhabricatorTextEditField' => 'applications/transactions/editfield/PhabricatorTextEditField.php',
'PhabricatorTextExportFormat' => 'infrastructure/export/format/PhabricatorTextExportFormat.php',
'PhabricatorTextListConfigType' => 'applications/config/type/PhabricatorTextListConfigType.php',
'PhabricatorTime' => 'infrastructure/time/PhabricatorTime.php',
'PhabricatorTimeFormatSetting' => 'applications/settings/setting/PhabricatorTimeFormatSetting.php',
'PhabricatorTimeGuard' => 'infrastructure/time/PhabricatorTimeGuard.php',
'PhabricatorTimeTestCase' => 'infrastructure/time/__tests__/PhabricatorTimeTestCase.php',
'PhabricatorTimelineEngine' => 'applications/transactions/engine/PhabricatorTimelineEngine.php',
'PhabricatorTimelineInterface' => 'applications/transactions/interface/PhabricatorTimelineInterface.php',
'PhabricatorTimezoneIgnoreOffsetSetting' => 'applications/settings/setting/PhabricatorTimezoneIgnoreOffsetSetting.php',
'PhabricatorTimezoneSetting' => 'applications/settings/setting/PhabricatorTimezoneSetting.php',
'PhabricatorTimezoneSetupCheck' => 'applications/config/check/PhabricatorTimezoneSetupCheck.php',
'PhabricatorTitleGlyphsSetting' => 'applications/settings/setting/PhabricatorTitleGlyphsSetting.php',
'PhabricatorToken' => 'applications/tokens/storage/PhabricatorToken.php',
'PhabricatorTokenController' => 'applications/tokens/controller/PhabricatorTokenController.php',
'PhabricatorTokenCount' => 'applications/tokens/storage/PhabricatorTokenCount.php',
'PhabricatorTokenCountQuery' => 'applications/tokens/query/PhabricatorTokenCountQuery.php',
'PhabricatorTokenDAO' => 'applications/tokens/storage/PhabricatorTokenDAO.php',
'PhabricatorTokenDestructionEngineExtension' => 'applications/tokens/engineextension/PhabricatorTokenDestructionEngineExtension.php',
'PhabricatorTokenGiveController' => 'applications/tokens/controller/PhabricatorTokenGiveController.php',
'PhabricatorTokenGiven' => 'applications/tokens/storage/PhabricatorTokenGiven.php',
'PhabricatorTokenGivenController' => 'applications/tokens/controller/PhabricatorTokenGivenController.php',
'PhabricatorTokenGivenEditor' => 'applications/tokens/editor/PhabricatorTokenGivenEditor.php',
'PhabricatorTokenGivenFeedStory' => 'applications/tokens/feed/PhabricatorTokenGivenFeedStory.php',
'PhabricatorTokenGivenQuery' => 'applications/tokens/query/PhabricatorTokenGivenQuery.php',
'PhabricatorTokenLeaderController' => 'applications/tokens/controller/PhabricatorTokenLeaderController.php',
'PhabricatorTokenQuery' => 'applications/tokens/query/PhabricatorTokenQuery.php',
'PhabricatorTokenReceiverInterface' => 'applications/tokens/interface/PhabricatorTokenReceiverInterface.php',
'PhabricatorTokenReceiverQuery' => 'applications/tokens/query/PhabricatorTokenReceiverQuery.php',
'PhabricatorTokenTokenPHIDType' => 'applications/tokens/phid/PhabricatorTokenTokenPHIDType.php',
'PhabricatorTokenUIEventListener' => 'applications/tokens/event/PhabricatorTokenUIEventListener.php',
'PhabricatorTokenizerEditField' => 'applications/transactions/editfield/PhabricatorTokenizerEditField.php',
'PhabricatorTokensApplication' => 'applications/tokens/application/PhabricatorTokensApplication.php',
'PhabricatorTokensCurtainExtension' => 'applications/tokens/engineextension/PhabricatorTokensCurtainExtension.php',
'PhabricatorTokensSettingsPanel' => 'applications/settings/panel/PhabricatorTokensSettingsPanel.php',
'PhabricatorTokensToken' => 'applications/tokens/storage/PhabricatorTokensToken.php',
'PhabricatorTransactionChange' => 'applications/transactions/data/PhabricatorTransactionChange.php',
'PhabricatorTransactionFactEngine' => 'applications/fact/engine/PhabricatorTransactionFactEngine.php',
'PhabricatorTransactionRemarkupChange' => 'applications/transactions/data/PhabricatorTransactionRemarkupChange.php',
'PhabricatorTransactions' => 'applications/transactions/constants/PhabricatorTransactions.php',
'PhabricatorTransactionsApplication' => 'applications/transactions/application/PhabricatorTransactionsApplication.php',
'PhabricatorTransactionsDestructionEngineExtension' => 'applications/transactions/engineextension/PhabricatorTransactionsDestructionEngineExtension.php',
'PhabricatorTransactionsFulltextEngineExtension' => 'applications/transactions/engineextension/PhabricatorTransactionsFulltextEngineExtension.php',
'PhabricatorTransformedFile' => 'applications/files/storage/PhabricatorTransformedFile.php',
'PhabricatorTranslationSetting' => 'applications/settings/setting/PhabricatorTranslationSetting.php',
'PhabricatorTranslationsConfigOptions' => 'applications/config/option/PhabricatorTranslationsConfigOptions.php',
'PhabricatorTriggerAction' => 'infrastructure/daemon/workers/action/PhabricatorTriggerAction.php',
'PhabricatorTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorTriggerClock.php',
'PhabricatorTriggerClockTestCase' => 'infrastructure/daemon/workers/clock/__tests__/PhabricatorTriggerClockTestCase.php',
'PhabricatorTriggerDaemon' => 'infrastructure/daemon/workers/PhabricatorTriggerDaemon.php',
'PhabricatorTrivialTestCase' => 'infrastructure/testing/__tests__/PhabricatorTrivialTestCase.php',
'PhabricatorTwilioFuture' => 'applications/metamta/future/PhabricatorTwilioFuture.php',
'PhabricatorTwitchAuthProvider' => 'applications/auth/provider/PhabricatorTwitchAuthProvider.php',
'PhabricatorTwitterAuthProvider' => 'applications/auth/provider/PhabricatorTwitterAuthProvider.php',
'PhabricatorTypeaheadApplication' => 'applications/typeahead/application/PhabricatorTypeaheadApplication.php',
'PhabricatorTypeaheadCompositeDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php',
'PhabricatorTypeaheadDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php',
'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadDatasourceController.php',
'PhabricatorTypeaheadDatasourceTestCase' => 'applications/typeahead/datasource/__tests__/PhabricatorTypeaheadDatasourceTestCase.php',
'PhabricatorTypeaheadFunctionHelpController' => 'applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php',
'PhabricatorTypeaheadInvalidTokenException' => 'applications/typeahead/exception/PhabricatorTypeaheadInvalidTokenException.php',
'PhabricatorTypeaheadModularDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php',
'PhabricatorTypeaheadMonogramDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadMonogramDatasource.php',
'PhabricatorTypeaheadProxyDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadProxyDatasource.php',
'PhabricatorTypeaheadResult' => 'applications/typeahead/storage/PhabricatorTypeaheadResult.php',
'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadRuntimeCompositeDatasource.php',
'PhabricatorTypeaheadTestNumbersDatasource' => 'applications/typeahead/datasource/__tests__/PhabricatorTypeaheadTestNumbersDatasource.php',
'PhabricatorTypeaheadTokenView' => 'applications/typeahead/view/PhabricatorTypeaheadTokenView.php',
'PhabricatorUIConfigOptions' => 'applications/config/option/PhabricatorUIConfigOptions.php',
'PhabricatorUIExample' => 'applications/uiexample/examples/PhabricatorUIExample.php',
'PhabricatorUIExampleRenderController' => 'applications/uiexample/controller/PhabricatorUIExampleRenderController.php',
'PhabricatorUIExamplesApplication' => 'applications/uiexample/application/PhabricatorUIExamplesApplication.php',
'PhabricatorURIExportField' => 'infrastructure/export/field/PhabricatorURIExportField.php',
'PhabricatorUSEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php',
'PhabricatorUnifiedDiffsSetting' => 'applications/settings/setting/PhabricatorUnifiedDiffsSetting.php',
'PhabricatorUnitTestContentSource' => 'infrastructure/contentsource/PhabricatorUnitTestContentSource.php',
'PhabricatorUnitsTestCase' => 'view/__tests__/PhabricatorUnitsTestCase.php',
'PhabricatorUnknownContentSource' => 'infrastructure/contentsource/PhabricatorUnknownContentSource.php',
+ 'PhabricatorUnlockEngine' => 'applications/system/engine/PhabricatorUnlockEngine.php',
+ 'PhabricatorUnlockableInterface' => 'applications/system/interface/PhabricatorUnlockableInterface.php',
'PhabricatorUnsubscribedFromObjectEdgeType' => 'applications/transactions/edges/PhabricatorUnsubscribedFromObjectEdgeType.php',
'PhabricatorUser' => 'applications/people/storage/PhabricatorUser.php',
'PhabricatorUserApproveTransaction' => 'applications/people/xaction/PhabricatorUserApproveTransaction.php',
'PhabricatorUserBadgesCacheType' => 'applications/people/cache/PhabricatorUserBadgesCacheType.php',
'PhabricatorUserBlurbField' => 'applications/people/customfield/PhabricatorUserBlurbField.php',
'PhabricatorUserCache' => 'applications/people/storage/PhabricatorUserCache.php',
'PhabricatorUserCachePurger' => 'applications/cache/purger/PhabricatorUserCachePurger.php',
'PhabricatorUserCacheType' => 'applications/people/cache/PhabricatorUserCacheType.php',
'PhabricatorUserCardView' => 'applications/people/view/PhabricatorUserCardView.php',
'PhabricatorUserConfigOptions' => 'applications/people/config/PhabricatorUserConfigOptions.php',
'PhabricatorUserConfiguredCustomField' => 'applications/people/customfield/PhabricatorUserConfiguredCustomField.php',
'PhabricatorUserConfiguredCustomFieldStorage' => 'applications/people/storage/PhabricatorUserConfiguredCustomFieldStorage.php',
'PhabricatorUserCustomField' => 'applications/people/customfield/PhabricatorUserCustomField.php',
'PhabricatorUserCustomFieldNumericIndex' => 'applications/people/storage/PhabricatorUserCustomFieldNumericIndex.php',
'PhabricatorUserCustomFieldStringIndex' => 'applications/people/storage/PhabricatorUserCustomFieldStringIndex.php',
'PhabricatorUserDAO' => 'applications/people/storage/PhabricatorUserDAO.php',
'PhabricatorUserDisableTransaction' => 'applications/people/xaction/PhabricatorUserDisableTransaction.php',
'PhabricatorUserEditEngine' => 'applications/people/editor/PhabricatorUserEditEngine.php',
'PhabricatorUserEditor' => 'applications/people/editor/PhabricatorUserEditor.php',
'PhabricatorUserEditorTestCase' => 'applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php',
'PhabricatorUserEmail' => 'applications/people/storage/PhabricatorUserEmail.php',
'PhabricatorUserEmailTestCase' => 'applications/people/storage/__tests__/PhabricatorUserEmailTestCase.php',
'PhabricatorUserEmpowerTransaction' => 'applications/people/xaction/PhabricatorUserEmpowerTransaction.php',
'PhabricatorUserFerretEngine' => 'applications/people/search/PhabricatorUserFerretEngine.php',
'PhabricatorUserFulltextEngine' => 'applications/people/search/PhabricatorUserFulltextEngine.php',
'PhabricatorUserIconField' => 'applications/people/customfield/PhabricatorUserIconField.php',
'PhabricatorUserLog' => 'applications/people/storage/PhabricatorUserLog.php',
'PhabricatorUserLogView' => 'applications/people/view/PhabricatorUserLogView.php',
'PhabricatorUserMessageCountCacheType' => 'applications/people/cache/PhabricatorUserMessageCountCacheType.php',
'PhabricatorUserNotificationCountCacheType' => 'applications/people/cache/PhabricatorUserNotificationCountCacheType.php',
'PhabricatorUserNotifyTransaction' => 'applications/people/xaction/PhabricatorUserNotifyTransaction.php',
'PhabricatorUserPHIDResolver' => 'applications/phid/resolver/PhabricatorUserPHIDResolver.php',
'PhabricatorUserPreferences' => 'applications/settings/storage/PhabricatorUserPreferences.php',
'PhabricatorUserPreferencesCacheType' => 'applications/people/cache/PhabricatorUserPreferencesCacheType.php',
'PhabricatorUserPreferencesEditor' => 'applications/settings/editor/PhabricatorUserPreferencesEditor.php',
'PhabricatorUserPreferencesPHIDType' => 'applications/settings/phid/PhabricatorUserPreferencesPHIDType.php',
'PhabricatorUserPreferencesQuery' => 'applications/settings/query/PhabricatorUserPreferencesQuery.php',
'PhabricatorUserPreferencesSearchEngine' => 'applications/settings/query/PhabricatorUserPreferencesSearchEngine.php',
'PhabricatorUserPreferencesTransaction' => 'applications/settings/storage/PhabricatorUserPreferencesTransaction.php',
'PhabricatorUserPreferencesTransactionQuery' => 'applications/settings/query/PhabricatorUserPreferencesTransactionQuery.php',
'PhabricatorUserProfile' => 'applications/people/storage/PhabricatorUserProfile.php',
'PhabricatorUserProfileImageCacheType' => 'applications/people/cache/PhabricatorUserProfileImageCacheType.php',
'PhabricatorUserRealNameField' => 'applications/people/customfield/PhabricatorUserRealNameField.php',
'PhabricatorUserRolesField' => 'applications/people/customfield/PhabricatorUserRolesField.php',
'PhabricatorUserSchemaSpec' => 'applications/people/storage/PhabricatorUserSchemaSpec.php',
'PhabricatorUserSinceField' => 'applications/people/customfield/PhabricatorUserSinceField.php',
'PhabricatorUserStatusField' => 'applications/people/customfield/PhabricatorUserStatusField.php',
'PhabricatorUserTestCase' => 'applications/people/storage/__tests__/PhabricatorUserTestCase.php',
'PhabricatorUserTitleField' => 'applications/people/customfield/PhabricatorUserTitleField.php',
'PhabricatorUserTransaction' => 'applications/people/storage/PhabricatorUserTransaction.php',
'PhabricatorUserTransactionEditor' => 'applications/people/editor/PhabricatorUserTransactionEditor.php',
'PhabricatorUserTransactionType' => 'applications/people/xaction/PhabricatorUserTransactionType.php',
'PhabricatorUserUsernameTransaction' => 'applications/people/xaction/PhabricatorUserUsernameTransaction.php',
'PhabricatorUsersEditField' => 'applications/transactions/editfield/PhabricatorUsersEditField.php',
'PhabricatorUsersPolicyRule' => 'applications/people/policyrule/PhabricatorUsersPolicyRule.php',
'PhabricatorUsersSearchField' => 'applications/people/searchfield/PhabricatorUsersSearchField.php',
'PhabricatorVCSResponse' => 'applications/repository/response/PhabricatorVCSResponse.php',
'PhabricatorVersionedDraft' => 'applications/draft/storage/PhabricatorVersionedDraft.php',
'PhabricatorVeryWowEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorVeryWowEnglishTranslation.php',
'PhabricatorVideoDocumentEngine' => 'applications/files/document/PhabricatorVideoDocumentEngine.php',
'PhabricatorViewerDatasource' => 'applications/people/typeahead/PhabricatorViewerDatasource.php',
'PhabricatorVoidDocumentEngine' => 'applications/files/document/PhabricatorVoidDocumentEngine.php',
'PhabricatorWatcherHasObjectEdgeType' => 'applications/transactions/edges/PhabricatorWatcherHasObjectEdgeType.php',
'PhabricatorWebContentSource' => 'infrastructure/contentsource/PhabricatorWebContentSource.php',
'PhabricatorWebServerSetupCheck' => 'applications/config/check/PhabricatorWebServerSetupCheck.php',
'PhabricatorWeekStartDaySetting' => 'applications/settings/setting/PhabricatorWeekStartDaySetting.php',
'PhabricatorWildConfigType' => 'applications/config/type/PhabricatorWildConfigType.php',
'PhabricatorWordPressAuthProvider' => 'applications/auth/provider/PhabricatorWordPressAuthProvider.php',
'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php',
'PhabricatorWorkerActiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php',
'PhabricatorWorkerActiveTaskQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerActiveTaskQuery.php',
'PhabricatorWorkerArchiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php',
'PhabricatorWorkerArchiveTaskQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerArchiveTaskQuery.php',
'PhabricatorWorkerBulkJob' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php',
'PhabricatorWorkerBulkJobCreateWorker' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobCreateWorker.php',
'PhabricatorWorkerBulkJobEditor' => 'infrastructure/daemon/workers/editor/PhabricatorWorkerBulkJobEditor.php',
'PhabricatorWorkerBulkJobPHIDType' => 'infrastructure/daemon/workers/phid/PhabricatorWorkerBulkJobPHIDType.php',
'PhabricatorWorkerBulkJobQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerBulkJobQuery.php',
'PhabricatorWorkerBulkJobSearchEngine' => 'infrastructure/daemon/workers/query/PhabricatorWorkerBulkJobSearchEngine.php',
'PhabricatorWorkerBulkJobTaskWorker' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobTaskWorker.php',
'PhabricatorWorkerBulkJobTestCase' => 'infrastructure/daemon/workers/__tests__/PhabricatorWorkerBulkJobTestCase.php',
'PhabricatorWorkerBulkJobTransaction' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJobTransaction.php',
'PhabricatorWorkerBulkJobTransactionQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerBulkJobTransactionQuery.php',
'PhabricatorWorkerBulkJobType' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobType.php',
'PhabricatorWorkerBulkJobWorker' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobWorker.php',
'PhabricatorWorkerBulkTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerBulkTask.php',
'PhabricatorWorkerDAO' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerDAO.php',
'PhabricatorWorkerDestructionEngineExtension' => 'infrastructure/daemon/workers/engineextension/PhabricatorWorkerDestructionEngineExtension.php',
'PhabricatorWorkerLeaseQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php',
'PhabricatorWorkerManagementCancelWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementCancelWorkflow.php',
'PhabricatorWorkerManagementExecuteWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementExecuteWorkflow.php',
'PhabricatorWorkerManagementFloodWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementFloodWorkflow.php',
'PhabricatorWorkerManagementFreeWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementFreeWorkflow.php',
'PhabricatorWorkerManagementRetryWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementRetryWorkflow.php',
'PhabricatorWorkerManagementWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementWorkflow.php',
'PhabricatorWorkerPermanentFailureException' => 'infrastructure/daemon/workers/exception/PhabricatorWorkerPermanentFailureException.php',
'PhabricatorWorkerSchemaSpec' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerSchemaSpec.php',
'PhabricatorWorkerSingleBulkJobType' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerSingleBulkJobType.php',
'PhabricatorWorkerTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php',
'PhabricatorWorkerTaskData' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTaskData.php',
'PhabricatorWorkerTaskDetailController' => 'applications/daemon/controller/PhabricatorWorkerTaskDetailController.php',
'PhabricatorWorkerTaskQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerTaskQuery.php',
'PhabricatorWorkerTestCase' => 'infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php',
'PhabricatorWorkerTrigger' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTrigger.php',
'PhabricatorWorkerTriggerEvent' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTriggerEvent.php',
'PhabricatorWorkerTriggerManagementFireWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementFireWorkflow.php',
'PhabricatorWorkerTriggerManagementWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementWorkflow.php',
'PhabricatorWorkerTriggerPHIDType' => 'infrastructure/daemon/workers/phid/PhabricatorWorkerTriggerPHIDType.php',
'PhabricatorWorkerTriggerQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php',
'PhabricatorWorkerYieldException' => 'infrastructure/daemon/workers/exception/PhabricatorWorkerYieldException.php',
'PhabricatorWorkingCopyDiscoveryTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyDiscoveryTestCase.php',
'PhabricatorWorkingCopyPullTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyPullTestCase.php',
'PhabricatorWorkingCopyTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyTestCase.php',
'PhabricatorXHPASTDAO' => 'applications/phpast/storage/PhabricatorXHPASTDAO.php',
'PhabricatorXHPASTParseTree' => 'applications/phpast/storage/PhabricatorXHPASTParseTree.php',
'PhabricatorXHPASTViewController' => 'applications/phpast/controller/PhabricatorXHPASTViewController.php',
'PhabricatorXHPASTViewFrameController' => 'applications/phpast/controller/PhabricatorXHPASTViewFrameController.php',
'PhabricatorXHPASTViewFramesetController' => 'applications/phpast/controller/PhabricatorXHPASTViewFramesetController.php',
'PhabricatorXHPASTViewInputController' => 'applications/phpast/controller/PhabricatorXHPASTViewInputController.php',
'PhabricatorXHPASTViewPanelController' => 'applications/phpast/controller/PhabricatorXHPASTViewPanelController.php',
'PhabricatorXHPASTViewRunController' => 'applications/phpast/controller/PhabricatorXHPASTViewRunController.php',
'PhabricatorXHPASTViewStreamController' => 'applications/phpast/controller/PhabricatorXHPASTViewStreamController.php',
'PhabricatorXHPASTViewTreeController' => 'applications/phpast/controller/PhabricatorXHPASTViewTreeController.php',
'PhabricatorXHProfApplication' => 'applications/xhprof/application/PhabricatorXHProfApplication.php',
'PhabricatorXHProfController' => 'applications/xhprof/controller/PhabricatorXHProfController.php',
'PhabricatorXHProfDAO' => 'applications/xhprof/storage/PhabricatorXHProfDAO.php',
'PhabricatorXHProfDropController' => 'applications/xhprof/controller/PhabricatorXHProfDropController.php',
'PhabricatorXHProfProfileController' => 'applications/xhprof/controller/PhabricatorXHProfProfileController.php',
'PhabricatorXHProfProfileSymbolView' => 'applications/xhprof/view/PhabricatorXHProfProfileSymbolView.php',
'PhabricatorXHProfProfileTopLevelView' => 'applications/xhprof/view/PhabricatorXHProfProfileTopLevelView.php',
'PhabricatorXHProfProfileView' => 'applications/xhprof/view/PhabricatorXHProfProfileView.php',
'PhabricatorXHProfSample' => 'applications/xhprof/storage/PhabricatorXHProfSample.php',
'PhabricatorXHProfSampleListController' => 'applications/xhprof/controller/PhabricatorXHProfSampleListController.php',
'PhabricatorXHProfSampleQuery' => 'applications/xhprof/query/PhabricatorXHProfSampleQuery.php',
'PhabricatorXHProfSampleSearchEngine' => 'applications/xhprof/query/PhabricatorXHProfSampleSearchEngine.php',
'PhabricatorYoutubeRemarkupRule' => 'infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php',
'Phame404Response' => 'applications/phame/site/Phame404Response.php',
'PhameBlog' => 'applications/phame/storage/PhameBlog.php',
'PhameBlog404Controller' => 'applications/phame/controller/blog/PhameBlog404Controller.php',
'PhameBlogArchiveController' => 'applications/phame/controller/blog/PhameBlogArchiveController.php',
'PhameBlogController' => 'applications/phame/controller/blog/PhameBlogController.php',
'PhameBlogCreateCapability' => 'applications/phame/capability/PhameBlogCreateCapability.php',
'PhameBlogDatasource' => 'applications/phame/typeahead/PhameBlogDatasource.php',
'PhameBlogDescriptionTransaction' => 'applications/phame/xaction/PhameBlogDescriptionTransaction.php',
'PhameBlogEditConduitAPIMethod' => 'applications/phame/conduit/PhameBlogEditConduitAPIMethod.php',
'PhameBlogEditController' => 'applications/phame/controller/blog/PhameBlogEditController.php',
'PhameBlogEditEngine' => 'applications/phame/editor/PhameBlogEditEngine.php',
'PhameBlogEditor' => 'applications/phame/editor/PhameBlogEditor.php',
'PhameBlogFeedController' => 'applications/phame/controller/blog/PhameBlogFeedController.php',
'PhameBlogFerretEngine' => 'applications/phame/search/PhameBlogFerretEngine.php',
'PhameBlogFullDomainTransaction' => 'applications/phame/xaction/PhameBlogFullDomainTransaction.php',
'PhameBlogFulltextEngine' => 'applications/phame/search/PhameBlogFulltextEngine.php',
'PhameBlogHeaderImageTransaction' => 'applications/phame/xaction/PhameBlogHeaderImageTransaction.php',
'PhameBlogHeaderPictureController' => 'applications/phame/controller/blog/PhameBlogHeaderPictureController.php',
'PhameBlogListController' => 'applications/phame/controller/blog/PhameBlogListController.php',
'PhameBlogListView' => 'applications/phame/view/PhameBlogListView.php',
'PhameBlogManageController' => 'applications/phame/controller/blog/PhameBlogManageController.php',
'PhameBlogNameTransaction' => 'applications/phame/xaction/PhameBlogNameTransaction.php',
'PhameBlogParentDomainTransaction' => 'applications/phame/xaction/PhameBlogParentDomainTransaction.php',
'PhameBlogParentSiteTransaction' => 'applications/phame/xaction/PhameBlogParentSiteTransaction.php',
'PhameBlogProfileImageTransaction' => 'applications/phame/xaction/PhameBlogProfileImageTransaction.php',
'PhameBlogProfilePictureController' => 'applications/phame/controller/blog/PhameBlogProfilePictureController.php',
'PhameBlogQuery' => 'applications/phame/query/PhameBlogQuery.php',
'PhameBlogReplyHandler' => 'applications/phame/mail/PhameBlogReplyHandler.php',
'PhameBlogSearchConduitAPIMethod' => 'applications/phame/conduit/PhameBlogSearchConduitAPIMethod.php',
'PhameBlogSearchEngine' => 'applications/phame/query/PhameBlogSearchEngine.php',
'PhameBlogSite' => 'applications/phame/site/PhameBlogSite.php',
'PhameBlogStatusTransaction' => 'applications/phame/xaction/PhameBlogStatusTransaction.php',
'PhameBlogSubtitleTransaction' => 'applications/phame/xaction/PhameBlogSubtitleTransaction.php',
'PhameBlogTransaction' => 'applications/phame/storage/PhameBlogTransaction.php',
'PhameBlogTransactionQuery' => 'applications/phame/query/PhameBlogTransactionQuery.php',
'PhameBlogTransactionType' => 'applications/phame/xaction/PhameBlogTransactionType.php',
'PhameBlogViewController' => 'applications/phame/controller/blog/PhameBlogViewController.php',
'PhameConstants' => 'applications/phame/constants/PhameConstants.php',
'PhameController' => 'applications/phame/controller/PhameController.php',
'PhameDAO' => 'applications/phame/storage/PhameDAO.php',
'PhameDescriptionView' => 'applications/phame/view/PhameDescriptionView.php',
'PhameDraftListView' => 'applications/phame/view/PhameDraftListView.php',
'PhameHomeController' => 'applications/phame/controller/PhameHomeController.php',
'PhameLiveController' => 'applications/phame/controller/PhameLiveController.php',
'PhameNextPostView' => 'applications/phame/view/PhameNextPostView.php',
'PhamePost' => 'applications/phame/storage/PhamePost.php',
'PhamePostArchiveController' => 'applications/phame/controller/post/PhamePostArchiveController.php',
'PhamePostBlogTransaction' => 'applications/phame/xaction/PhamePostBlogTransaction.php',
'PhamePostBodyTransaction' => 'applications/phame/xaction/PhamePostBodyTransaction.php',
'PhamePostController' => 'applications/phame/controller/post/PhamePostController.php',
'PhamePostEditConduitAPIMethod' => 'applications/phame/conduit/PhamePostEditConduitAPIMethod.php',
'PhamePostEditController' => 'applications/phame/controller/post/PhamePostEditController.php',
'PhamePostEditEngine' => 'applications/phame/editor/PhamePostEditEngine.php',
'PhamePostEditor' => 'applications/phame/editor/PhamePostEditor.php',
'PhamePostFerretEngine' => 'applications/phame/search/PhamePostFerretEngine.php',
'PhamePostFulltextEngine' => 'applications/phame/search/PhamePostFulltextEngine.php',
'PhamePostHeaderImageTransaction' => 'applications/phame/xaction/PhamePostHeaderImageTransaction.php',
'PhamePostHeaderPictureController' => 'applications/phame/controller/post/PhamePostHeaderPictureController.php',
'PhamePostHistoryController' => 'applications/phame/controller/post/PhamePostHistoryController.php',
'PhamePostListController' => 'applications/phame/controller/post/PhamePostListController.php',
'PhamePostListView' => 'applications/phame/view/PhamePostListView.php',
'PhamePostMailReceiver' => 'applications/phame/mail/PhamePostMailReceiver.php',
'PhamePostMoveController' => 'applications/phame/controller/post/PhamePostMoveController.php',
'PhamePostPublishController' => 'applications/phame/controller/post/PhamePostPublishController.php',
'PhamePostQuery' => 'applications/phame/query/PhamePostQuery.php',
'PhamePostRemarkupRule' => 'applications/phame/remarkup/PhamePostRemarkupRule.php',
'PhamePostReplyHandler' => 'applications/phame/mail/PhamePostReplyHandler.php',
'PhamePostSearchConduitAPIMethod' => 'applications/phame/conduit/PhamePostSearchConduitAPIMethod.php',
'PhamePostSearchEngine' => 'applications/phame/query/PhamePostSearchEngine.php',
'PhamePostSubtitleTransaction' => 'applications/phame/xaction/PhamePostSubtitleTransaction.php',
'PhamePostTitleTransaction' => 'applications/phame/xaction/PhamePostTitleTransaction.php',
'PhamePostTransaction' => 'applications/phame/storage/PhamePostTransaction.php',
'PhamePostTransactionComment' => 'applications/phame/storage/PhamePostTransactionComment.php',
'PhamePostTransactionQuery' => 'applications/phame/query/PhamePostTransactionQuery.php',
'PhamePostTransactionType' => 'applications/phame/xaction/PhamePostTransactionType.php',
'PhamePostViewController' => 'applications/phame/controller/post/PhamePostViewController.php',
'PhamePostVisibilityTransaction' => 'applications/phame/xaction/PhamePostVisibilityTransaction.php',
'PhameSchemaSpec' => 'applications/phame/storage/PhameSchemaSpec.php',
'PhameSite' => 'applications/phame/site/PhameSite.php',
'PhluxController' => 'applications/phlux/controller/PhluxController.php',
'PhluxDAO' => 'applications/phlux/storage/PhluxDAO.php',
'PhluxEditController' => 'applications/phlux/controller/PhluxEditController.php',
'PhluxListController' => 'applications/phlux/controller/PhluxListController.php',
'PhluxSchemaSpec' => 'applications/phlux/storage/PhluxSchemaSpec.php',
'PhluxTransaction' => 'applications/phlux/storage/PhluxTransaction.php',
'PhluxTransactionQuery' => 'applications/phlux/query/PhluxTransactionQuery.php',
'PhluxVariable' => 'applications/phlux/storage/PhluxVariable.php',
'PhluxVariableEditor' => 'applications/phlux/editor/PhluxVariableEditor.php',
'PhluxVariablePHIDType' => 'applications/phlux/phid/PhluxVariablePHIDType.php',
'PhluxVariableQuery' => 'applications/phlux/query/PhluxVariableQuery.php',
'PhluxViewController' => 'applications/phlux/controller/PhluxViewController.php',
'PholioController' => 'applications/pholio/controller/PholioController.php',
'PholioDAO' => 'applications/pholio/storage/PholioDAO.php',
'PholioDefaultEditCapability' => 'applications/pholio/capability/PholioDefaultEditCapability.php',
'PholioDefaultViewCapability' => 'applications/pholio/capability/PholioDefaultViewCapability.php',
'PholioImage' => 'applications/pholio/storage/PholioImage.php',
'PholioImageDescriptionTransaction' => 'applications/pholio/xaction/PholioImageDescriptionTransaction.php',
'PholioImageFileTransaction' => 'applications/pholio/xaction/PholioImageFileTransaction.php',
'PholioImageNameTransaction' => 'applications/pholio/xaction/PholioImageNameTransaction.php',
'PholioImagePHIDType' => 'applications/pholio/phid/PholioImagePHIDType.php',
'PholioImageQuery' => 'applications/pholio/query/PholioImageQuery.php',
'PholioImageReplaceTransaction' => 'applications/pholio/xaction/PholioImageReplaceTransaction.php',
'PholioImageSequenceTransaction' => 'applications/pholio/xaction/PholioImageSequenceTransaction.php',
'PholioImageTransactionType' => 'applications/pholio/xaction/PholioImageTransactionType.php',
'PholioImageUploadController' => 'applications/pholio/controller/PholioImageUploadController.php',
'PholioInlineController' => 'applications/pholio/controller/PholioInlineController.php',
'PholioInlineListController' => 'applications/pholio/controller/PholioInlineListController.php',
'PholioMock' => 'applications/pholio/storage/PholioMock.php',
'PholioMockArchiveController' => 'applications/pholio/controller/PholioMockArchiveController.php',
'PholioMockAuthorHeraldField' => 'applications/pholio/herald/PholioMockAuthorHeraldField.php',
'PholioMockCommentController' => 'applications/pholio/controller/PholioMockCommentController.php',
'PholioMockDescriptionHeraldField' => 'applications/pholio/herald/PholioMockDescriptionHeraldField.php',
'PholioMockDescriptionTransaction' => 'applications/pholio/xaction/PholioMockDescriptionTransaction.php',
'PholioMockEditController' => 'applications/pholio/controller/PholioMockEditController.php',
'PholioMockEditor' => 'applications/pholio/editor/PholioMockEditor.php',
'PholioMockEmbedView' => 'applications/pholio/view/PholioMockEmbedView.php',
'PholioMockFerretEngine' => 'applications/pholio/search/PholioMockFerretEngine.php',
'PholioMockFulltextEngine' => 'applications/pholio/search/PholioMockFulltextEngine.php',
'PholioMockHasTaskEdgeType' => 'applications/pholio/edge/PholioMockHasTaskEdgeType.php',
'PholioMockHasTaskRelationship' => 'applications/pholio/relationships/PholioMockHasTaskRelationship.php',
'PholioMockHeraldField' => 'applications/pholio/herald/PholioMockHeraldField.php',
'PholioMockHeraldFieldGroup' => 'applications/pholio/herald/PholioMockHeraldFieldGroup.php',
'PholioMockImagesView' => 'applications/pholio/view/PholioMockImagesView.php',
'PholioMockInlineTransaction' => 'applications/pholio/xaction/PholioMockInlineTransaction.php',
'PholioMockListController' => 'applications/pholio/controller/PholioMockListController.php',
'PholioMockMailReceiver' => 'applications/pholio/mail/PholioMockMailReceiver.php',
'PholioMockNameHeraldField' => 'applications/pholio/herald/PholioMockNameHeraldField.php',
'PholioMockNameTransaction' => 'applications/pholio/xaction/PholioMockNameTransaction.php',
'PholioMockPHIDType' => 'applications/pholio/phid/PholioMockPHIDType.php',
'PholioMockQuery' => 'applications/pholio/query/PholioMockQuery.php',
'PholioMockRelationship' => 'applications/pholio/relationships/PholioMockRelationship.php',
'PholioMockRelationshipSource' => 'applications/search/relationship/PholioMockRelationshipSource.php',
'PholioMockSearchEngine' => 'applications/pholio/query/PholioMockSearchEngine.php',
'PholioMockStatusTransaction' => 'applications/pholio/xaction/PholioMockStatusTransaction.php',
'PholioMockThumbGridView' => 'applications/pholio/view/PholioMockThumbGridView.php',
'PholioMockTimelineEngine' => 'applications/pholio/engine/PholioMockTimelineEngine.php',
'PholioMockTransactionType' => 'applications/pholio/xaction/PholioMockTransactionType.php',
'PholioMockViewController' => 'applications/pholio/controller/PholioMockViewController.php',
'PholioRemarkupRule' => 'applications/pholio/remarkup/PholioRemarkupRule.php',
'PholioReplyHandler' => 'applications/pholio/mail/PholioReplyHandler.php',
'PholioSchemaSpec' => 'applications/pholio/storage/PholioSchemaSpec.php',
'PholioTransaction' => 'applications/pholio/storage/PholioTransaction.php',
'PholioTransactionComment' => 'applications/pholio/storage/PholioTransactionComment.php',
'PholioTransactionQuery' => 'applications/pholio/query/PholioTransactionQuery.php',
'PholioTransactionType' => 'applications/pholio/xaction/PholioTransactionType.php',
'PholioTransactionView' => 'applications/pholio/view/PholioTransactionView.php',
'PholioUploadedImageView' => 'applications/pholio/view/PholioUploadedImageView.php',
'PhortuneAccount' => 'applications/phortune/storage/PhortuneAccount.php',
'PhortuneAccountAddManagerController' => 'applications/phortune/controller/account/PhortuneAccountAddManagerController.php',
'PhortuneAccountBillingAddressTransaction' => 'applications/phortune/xaction/PhortuneAccountBillingAddressTransaction.php',
'PhortuneAccountBillingController' => 'applications/phortune/controller/account/PhortuneAccountBillingController.php',
'PhortuneAccountBillingNameTransaction' => 'applications/phortune/xaction/PhortuneAccountBillingNameTransaction.php',
'PhortuneAccountChargeListController' => 'applications/phortune/controller/account/PhortuneAccountChargeListController.php',
'PhortuneAccountController' => 'applications/phortune/controller/account/PhortuneAccountController.php',
'PhortuneAccountEditController' => 'applications/phortune/controller/account/PhortuneAccountEditController.php',
'PhortuneAccountEditEngine' => 'applications/phortune/editor/PhortuneAccountEditEngine.php',
'PhortuneAccountEditor' => 'applications/phortune/editor/PhortuneAccountEditor.php',
'PhortuneAccountHasMemberEdgeType' => 'applications/phortune/edge/PhortuneAccountHasMemberEdgeType.php',
'PhortuneAccountListController' => 'applications/phortune/controller/account/PhortuneAccountListController.php',
'PhortuneAccountManagerController' => 'applications/phortune/controller/account/PhortuneAccountManagerController.php',
'PhortuneAccountNameTransaction' => 'applications/phortune/xaction/PhortuneAccountNameTransaction.php',
'PhortuneAccountPHIDType' => 'applications/phortune/phid/PhortuneAccountPHIDType.php',
'PhortuneAccountProfileController' => 'applications/phortune/controller/account/PhortuneAccountProfileController.php',
'PhortuneAccountQuery' => 'applications/phortune/query/PhortuneAccountQuery.php',
'PhortuneAccountSubscriptionController' => 'applications/phortune/controller/account/PhortuneAccountSubscriptionController.php',
'PhortuneAccountTransaction' => 'applications/phortune/storage/PhortuneAccountTransaction.php',
'PhortuneAccountTransactionQuery' => 'applications/phortune/query/PhortuneAccountTransactionQuery.php',
'PhortuneAccountTransactionType' => 'applications/phortune/xaction/PhortuneAccountTransactionType.php',
'PhortuneAccountViewController' => 'applications/phortune/controller/account/PhortuneAccountViewController.php',
'PhortuneAdHocCart' => 'applications/phortune/cart/PhortuneAdHocCart.php',
'PhortuneAdHocProduct' => 'applications/phortune/product/PhortuneAdHocProduct.php',
+ 'PhortuneAddPaymentMethodAction' => 'applications/phortune/action/PhortuneAddPaymentMethodAction.php',
'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php',
'PhortuneCartAcceptController' => 'applications/phortune/controller/cart/PhortuneCartAcceptController.php',
'PhortuneCartCancelController' => 'applications/phortune/controller/cart/PhortuneCartCancelController.php',
'PhortuneCartCheckoutController' => 'applications/phortune/controller/cart/PhortuneCartCheckoutController.php',
'PhortuneCartController' => 'applications/phortune/controller/cart/PhortuneCartController.php',
'PhortuneCartEditor' => 'applications/phortune/editor/PhortuneCartEditor.php',
'PhortuneCartImplementation' => 'applications/phortune/cart/PhortuneCartImplementation.php',
'PhortuneCartListController' => 'applications/phortune/controller/cart/PhortuneCartListController.php',
'PhortuneCartPHIDType' => 'applications/phortune/phid/PhortuneCartPHIDType.php',
'PhortuneCartQuery' => 'applications/phortune/query/PhortuneCartQuery.php',
'PhortuneCartReplyHandler' => 'applications/phortune/mail/PhortuneCartReplyHandler.php',
'PhortuneCartSearchEngine' => 'applications/phortune/query/PhortuneCartSearchEngine.php',
'PhortuneCartTransaction' => 'applications/phortune/storage/PhortuneCartTransaction.php',
'PhortuneCartTransactionQuery' => 'applications/phortune/query/PhortuneCartTransactionQuery.php',
'PhortuneCartUpdateController' => 'applications/phortune/controller/cart/PhortuneCartUpdateController.php',
'PhortuneCartViewController' => 'applications/phortune/controller/cart/PhortuneCartViewController.php',
'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php',
'PhortuneChargePHIDType' => 'applications/phortune/phid/PhortuneChargePHIDType.php',
'PhortuneChargeQuery' => 'applications/phortune/query/PhortuneChargeQuery.php',
'PhortuneChargeSearchEngine' => 'applications/phortune/query/PhortuneChargeSearchEngine.php',
'PhortuneChargeTableView' => 'applications/phortune/view/PhortuneChargeTableView.php',
'PhortuneConstants' => 'applications/phortune/constants/PhortuneConstants.php',
'PhortuneController' => 'applications/phortune/controller/PhortuneController.php',
'PhortuneCreditCardForm' => 'applications/phortune/view/PhortuneCreditCardForm.php',
'PhortuneCurrency' => 'applications/phortune/currency/PhortuneCurrency.php',
'PhortuneCurrencySerializer' => 'applications/phortune/currency/PhortuneCurrencySerializer.php',
'PhortuneCurrencyTestCase' => 'applications/phortune/currency/__tests__/PhortuneCurrencyTestCase.php',
'PhortuneDAO' => 'applications/phortune/storage/PhortuneDAO.php',
+ 'PhortuneDisplayException' => 'applications/phortune/exception/PhortuneDisplayException.php',
'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php',
'PhortuneInvoiceView' => 'applications/phortune/view/PhortuneInvoiceView.php',
'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php',
'PhortuneMemberHasAccountEdgeType' => 'applications/phortune/edge/PhortuneMemberHasAccountEdgeType.php',
'PhortuneMemberHasMerchantEdgeType' => 'applications/phortune/edge/PhortuneMemberHasMerchantEdgeType.php',
'PhortuneMerchant' => 'applications/phortune/storage/PhortuneMerchant.php',
'PhortuneMerchantAddManagerController' => 'applications/phortune/controller/merchant/PhortuneMerchantAddManagerController.php',
'PhortuneMerchantCapability' => 'applications/phortune/capability/PhortuneMerchantCapability.php',
'PhortuneMerchantContactInfoTransaction' => 'applications/phortune/xaction/PhortuneMerchantContactInfoTransaction.php',
'PhortuneMerchantController' => 'applications/phortune/controller/merchant/PhortuneMerchantController.php',
'PhortuneMerchantDescriptionTransaction' => 'applications/phortune/xaction/PhortuneMerchantDescriptionTransaction.php',
'PhortuneMerchantEditController' => 'applications/phortune/controller/merchant/PhortuneMerchantEditController.php',
'PhortuneMerchantEditEngine' => 'applications/phortune/editor/PhortuneMerchantEditEngine.php',
'PhortuneMerchantEditor' => 'applications/phortune/editor/PhortuneMerchantEditor.php',
'PhortuneMerchantHasMemberEdgeType' => 'applications/phortune/edge/PhortuneMerchantHasMemberEdgeType.php',
'PhortuneMerchantInvoiceCreateController' => 'applications/phortune/controller/merchant/PhortuneMerchantInvoiceCreateController.php',
'PhortuneMerchantInvoiceEmailTransaction' => 'applications/phortune/xaction/PhortuneMerchantInvoiceEmailTransaction.php',
'PhortuneMerchantInvoiceFooterTransaction' => 'applications/phortune/xaction/PhortuneMerchantInvoiceFooterTransaction.php',
'PhortuneMerchantListController' => 'applications/phortune/controller/merchant/PhortuneMerchantListController.php',
'PhortuneMerchantManagerController' => 'applications/phortune/controller/merchant/PhortuneMerchantManagerController.php',
'PhortuneMerchantNameTransaction' => 'applications/phortune/xaction/PhortuneMerchantNameTransaction.php',
'PhortuneMerchantPHIDType' => 'applications/phortune/phid/PhortuneMerchantPHIDType.php',
'PhortuneMerchantPictureController' => 'applications/phortune/controller/merchant/PhortuneMerchantPictureController.php',
'PhortuneMerchantPictureTransaction' => 'applications/phortune/xaction/PhortuneMerchantPictureTransaction.php',
'PhortuneMerchantProfileController' => 'applications/phortune/controller/merchant/PhortuneMerchantProfileController.php',
'PhortuneMerchantQuery' => 'applications/phortune/query/PhortuneMerchantQuery.php',
'PhortuneMerchantSearchEngine' => 'applications/phortune/query/PhortuneMerchantSearchEngine.php',
'PhortuneMerchantTransaction' => 'applications/phortune/storage/PhortuneMerchantTransaction.php',
'PhortuneMerchantTransactionQuery' => 'applications/phortune/query/PhortuneMerchantTransactionQuery.php',
'PhortuneMerchantTransactionType' => 'applications/phortune/xaction/PhortuneMerchantTransactionType.php',
'PhortuneMerchantViewController' => 'applications/phortune/controller/merchant/PhortuneMerchantViewController.php',
'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php',
'PhortuneOrderTableView' => 'applications/phortune/view/PhortuneOrderTableView.php',
'PhortunePayPalPaymentProvider' => 'applications/phortune/provider/PhortunePayPalPaymentProvider.php',
'PhortunePaymentMethod' => 'applications/phortune/storage/PhortunePaymentMethod.php',
'PhortunePaymentMethodCreateController' => 'applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php',
'PhortunePaymentMethodDisableController' => 'applications/phortune/controller/payment/PhortunePaymentMethodDisableController.php',
'PhortunePaymentMethodEditController' => 'applications/phortune/controller/payment/PhortunePaymentMethodEditController.php',
'PhortunePaymentMethodPHIDType' => 'applications/phortune/phid/PhortunePaymentMethodPHIDType.php',
'PhortunePaymentMethodQuery' => 'applications/phortune/query/PhortunePaymentMethodQuery.php',
'PhortunePaymentProvider' => 'applications/phortune/provider/PhortunePaymentProvider.php',
'PhortunePaymentProviderConfig' => 'applications/phortune/storage/PhortunePaymentProviderConfig.php',
'PhortunePaymentProviderConfigEditor' => 'applications/phortune/editor/PhortunePaymentProviderConfigEditor.php',
'PhortunePaymentProviderConfigQuery' => 'applications/phortune/query/PhortunePaymentProviderConfigQuery.php',
'PhortunePaymentProviderConfigTransaction' => 'applications/phortune/storage/PhortunePaymentProviderConfigTransaction.php',
'PhortunePaymentProviderConfigTransactionQuery' => 'applications/phortune/query/PhortunePaymentProviderConfigTransactionQuery.php',
'PhortunePaymentProviderPHIDType' => 'applications/phortune/phid/PhortunePaymentProviderPHIDType.php',
'PhortunePaymentProviderTestCase' => 'applications/phortune/provider/__tests__/PhortunePaymentProviderTestCase.php',
'PhortuneProduct' => 'applications/phortune/storage/PhortuneProduct.php',
'PhortuneProductImplementation' => 'applications/phortune/product/PhortuneProductImplementation.php',
'PhortuneProductListController' => 'applications/phortune/controller/product/PhortuneProductListController.php',
'PhortuneProductPHIDType' => 'applications/phortune/phid/PhortuneProductPHIDType.php',
'PhortuneProductQuery' => 'applications/phortune/query/PhortuneProductQuery.php',
'PhortuneProductViewController' => 'applications/phortune/controller/product/PhortuneProductViewController.php',
'PhortuneProviderActionController' => 'applications/phortune/controller/provider/PhortuneProviderActionController.php',
'PhortuneProviderDisableController' => 'applications/phortune/controller/provider/PhortuneProviderDisableController.php',
'PhortuneProviderEditController' => 'applications/phortune/controller/provider/PhortuneProviderEditController.php',
'PhortunePurchase' => 'applications/phortune/storage/PhortunePurchase.php',
'PhortunePurchasePHIDType' => 'applications/phortune/phid/PhortunePurchasePHIDType.php',
'PhortunePurchaseQuery' => 'applications/phortune/query/PhortunePurchaseQuery.php',
'PhortuneSchemaSpec' => 'applications/phortune/storage/PhortuneSchemaSpec.php',
'PhortuneStripePaymentProvider' => 'applications/phortune/provider/PhortuneStripePaymentProvider.php',
'PhortuneSubscription' => 'applications/phortune/storage/PhortuneSubscription.php',
'PhortuneSubscriptionCart' => 'applications/phortune/cart/PhortuneSubscriptionCart.php',
'PhortuneSubscriptionEditController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php',
'PhortuneSubscriptionImplementation' => 'applications/phortune/subscription/PhortuneSubscriptionImplementation.php',
'PhortuneSubscriptionListController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionListController.php',
'PhortuneSubscriptionPHIDType' => 'applications/phortune/phid/PhortuneSubscriptionPHIDType.php',
'PhortuneSubscriptionProduct' => 'applications/phortune/product/PhortuneSubscriptionProduct.php',
'PhortuneSubscriptionQuery' => 'applications/phortune/query/PhortuneSubscriptionQuery.php',
'PhortuneSubscriptionSearchEngine' => 'applications/phortune/query/PhortuneSubscriptionSearchEngine.php',
'PhortuneSubscriptionTableView' => 'applications/phortune/view/PhortuneSubscriptionTableView.php',
'PhortuneSubscriptionViewController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionViewController.php',
'PhortuneSubscriptionWorker' => 'applications/phortune/worker/PhortuneSubscriptionWorker.php',
'PhortuneTestPaymentProvider' => 'applications/phortune/provider/PhortuneTestPaymentProvider.php',
'PhortuneWePayPaymentProvider' => 'applications/phortune/provider/PhortuneWePayPaymentProvider.php',
'PhragmentBrowseController' => 'applications/phragment/controller/PhragmentBrowseController.php',
'PhragmentCanCreateCapability' => 'applications/phragment/capability/PhragmentCanCreateCapability.php',
'PhragmentConduitAPIMethod' => 'applications/phragment/conduit/PhragmentConduitAPIMethod.php',
'PhragmentController' => 'applications/phragment/controller/PhragmentController.php',
'PhragmentCreateController' => 'applications/phragment/controller/PhragmentCreateController.php',
'PhragmentDAO' => 'applications/phragment/storage/PhragmentDAO.php',
'PhragmentFragment' => 'applications/phragment/storage/PhragmentFragment.php',
'PhragmentFragmentPHIDType' => 'applications/phragment/phid/PhragmentFragmentPHIDType.php',
'PhragmentFragmentQuery' => 'applications/phragment/query/PhragmentFragmentQuery.php',
'PhragmentFragmentVersion' => 'applications/phragment/storage/PhragmentFragmentVersion.php',
'PhragmentFragmentVersionPHIDType' => 'applications/phragment/phid/PhragmentFragmentVersionPHIDType.php',
'PhragmentFragmentVersionQuery' => 'applications/phragment/query/PhragmentFragmentVersionQuery.php',
'PhragmentGetPatchConduitAPIMethod' => 'applications/phragment/conduit/PhragmentGetPatchConduitAPIMethod.php',
'PhragmentHistoryController' => 'applications/phragment/controller/PhragmentHistoryController.php',
'PhragmentPatchController' => 'applications/phragment/controller/PhragmentPatchController.php',
'PhragmentPatchUtil' => 'applications/phragment/util/PhragmentPatchUtil.php',
'PhragmentPolicyController' => 'applications/phragment/controller/PhragmentPolicyController.php',
'PhragmentQueryFragmentsConduitAPIMethod' => 'applications/phragment/conduit/PhragmentQueryFragmentsConduitAPIMethod.php',
'PhragmentRevertController' => 'applications/phragment/controller/PhragmentRevertController.php',
'PhragmentSchemaSpec' => 'applications/phragment/storage/PhragmentSchemaSpec.php',
'PhragmentSnapshot' => 'applications/phragment/storage/PhragmentSnapshot.php',
'PhragmentSnapshotChild' => 'applications/phragment/storage/PhragmentSnapshotChild.php',
'PhragmentSnapshotChildQuery' => 'applications/phragment/query/PhragmentSnapshotChildQuery.php',
'PhragmentSnapshotCreateController' => 'applications/phragment/controller/PhragmentSnapshotCreateController.php',
'PhragmentSnapshotDeleteController' => 'applications/phragment/controller/PhragmentSnapshotDeleteController.php',
'PhragmentSnapshotPHIDType' => 'applications/phragment/phid/PhragmentSnapshotPHIDType.php',
'PhragmentSnapshotPromoteController' => 'applications/phragment/controller/PhragmentSnapshotPromoteController.php',
'PhragmentSnapshotQuery' => 'applications/phragment/query/PhragmentSnapshotQuery.php',
'PhragmentSnapshotViewController' => 'applications/phragment/controller/PhragmentSnapshotViewController.php',
'PhragmentUpdateController' => 'applications/phragment/controller/PhragmentUpdateController.php',
'PhragmentVersionController' => 'applications/phragment/controller/PhragmentVersionController.php',
'PhragmentZIPController' => 'applications/phragment/controller/PhragmentZIPController.php',
'PhrequentConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentConduitAPIMethod.php',
'PhrequentController' => 'applications/phrequent/controller/PhrequentController.php',
'PhrequentCurtainExtension' => 'applications/phrequent/engineextension/PhrequentCurtainExtension.php',
'PhrequentDAO' => 'applications/phrequent/storage/PhrequentDAO.php',
'PhrequentListController' => 'applications/phrequent/controller/PhrequentListController.php',
'PhrequentPopConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentPopConduitAPIMethod.php',
'PhrequentPushConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentPushConduitAPIMethod.php',
'PhrequentSearchEngine' => 'applications/phrequent/query/PhrequentSearchEngine.php',
'PhrequentTimeBlock' => 'applications/phrequent/storage/PhrequentTimeBlock.php',
'PhrequentTimeBlockTestCase' => 'applications/phrequent/storage/__tests__/PhrequentTimeBlockTestCase.php',
'PhrequentTimeSlices' => 'applications/phrequent/storage/PhrequentTimeSlices.php',
'PhrequentTrackController' => 'applications/phrequent/controller/PhrequentTrackController.php',
'PhrequentTrackableInterface' => 'applications/phrequent/interface/PhrequentTrackableInterface.php',
'PhrequentTrackingConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentTrackingConduitAPIMethod.php',
'PhrequentTrackingEditor' => 'applications/phrequent/editor/PhrequentTrackingEditor.php',
'PhrequentUIEventListener' => 'applications/phrequent/event/PhrequentUIEventListener.php',
'PhrequentUserTime' => 'applications/phrequent/storage/PhrequentUserTime.php',
'PhrequentUserTimeQuery' => 'applications/phrequent/query/PhrequentUserTimeQuery.php',
'PhrictionChangeType' => 'applications/phriction/constants/PhrictionChangeType.php',
'PhrictionConduitAPIMethod' => 'applications/phriction/conduit/PhrictionConduitAPIMethod.php',
'PhrictionConstants' => 'applications/phriction/constants/PhrictionConstants.php',
'PhrictionContent' => 'applications/phriction/storage/PhrictionContent.php',
'PhrictionContentPHIDType' => 'applications/phriction/phid/PhrictionContentPHIDType.php',
'PhrictionContentQuery' => 'applications/phriction/query/PhrictionContentQuery.php',
'PhrictionContentSearchConduitAPIMethod' => 'applications/phriction/conduit/PhrictionContentSearchConduitAPIMethod.php',
'PhrictionContentSearchEngine' => 'applications/phriction/query/PhrictionContentSearchEngine.php',
'PhrictionContentSearchEngineAttachment' => 'applications/phriction/engineextension/PhrictionContentSearchEngineAttachment.php',
'PhrictionController' => 'applications/phriction/controller/PhrictionController.php',
'PhrictionCreateConduitAPIMethod' => 'applications/phriction/conduit/PhrictionCreateConduitAPIMethod.php',
'PhrictionDAO' => 'applications/phriction/storage/PhrictionDAO.php',
'PhrictionDatasourceEngineExtension' => 'applications/phriction/engineextension/PhrictionDatasourceEngineExtension.php',
'PhrictionDeleteController' => 'applications/phriction/controller/PhrictionDeleteController.php',
'PhrictionDiffController' => 'applications/phriction/controller/PhrictionDiffController.php',
'PhrictionDocument' => 'applications/phriction/storage/PhrictionDocument.php',
'PhrictionDocumentAuthorHeraldField' => 'applications/phriction/herald/PhrictionDocumentAuthorHeraldField.php',
'PhrictionDocumentContentHeraldField' => 'applications/phriction/herald/PhrictionDocumentContentHeraldField.php',
'PhrictionDocumentContentTransaction' => 'applications/phriction/xaction/PhrictionDocumentContentTransaction.php',
'PhrictionDocumentController' => 'applications/phriction/controller/PhrictionDocumentController.php',
'PhrictionDocumentDatasource' => 'applications/phriction/typeahead/PhrictionDocumentDatasource.php',
'PhrictionDocumentDeleteTransaction' => 'applications/phriction/xaction/PhrictionDocumentDeleteTransaction.php',
'PhrictionDocumentDraftTransaction' => 'applications/phriction/xaction/PhrictionDocumentDraftTransaction.php',
'PhrictionDocumentEditEngine' => 'applications/phriction/editor/PhrictionDocumentEditEngine.php',
'PhrictionDocumentEditTransaction' => 'applications/phriction/xaction/PhrictionDocumentEditTransaction.php',
'PhrictionDocumentFerretEngine' => 'applications/phriction/search/PhrictionDocumentFerretEngine.php',
'PhrictionDocumentFulltextEngine' => 'applications/phriction/search/PhrictionDocumentFulltextEngine.php',
'PhrictionDocumentHeraldAdapter' => 'applications/phriction/herald/PhrictionDocumentHeraldAdapter.php',
'PhrictionDocumentHeraldField' => 'applications/phriction/herald/PhrictionDocumentHeraldField.php',
'PhrictionDocumentHeraldFieldGroup' => 'applications/phriction/herald/PhrictionDocumentHeraldFieldGroup.php',
'PhrictionDocumentMoveAwayTransaction' => 'applications/phriction/xaction/PhrictionDocumentMoveAwayTransaction.php',
'PhrictionDocumentMoveToTransaction' => 'applications/phriction/xaction/PhrictionDocumentMoveToTransaction.php',
'PhrictionDocumentPHIDType' => 'applications/phriction/phid/PhrictionDocumentPHIDType.php',
'PhrictionDocumentPathHeraldField' => 'applications/phriction/herald/PhrictionDocumentPathHeraldField.php',
'PhrictionDocumentPolicyCodex' => 'applications/phriction/codex/PhrictionDocumentPolicyCodex.php',
'PhrictionDocumentPublishTransaction' => 'applications/phriction/xaction/PhrictionDocumentPublishTransaction.php',
'PhrictionDocumentQuery' => 'applications/phriction/query/PhrictionDocumentQuery.php',
'PhrictionDocumentSearchConduitAPIMethod' => 'applications/phriction/conduit/PhrictionDocumentSearchConduitAPIMethod.php',
'PhrictionDocumentSearchEngine' => 'applications/phriction/query/PhrictionDocumentSearchEngine.php',
'PhrictionDocumentStatus' => 'applications/phriction/constants/PhrictionDocumentStatus.php',
'PhrictionDocumentTitleHeraldField' => 'applications/phriction/herald/PhrictionDocumentTitleHeraldField.php',
'PhrictionDocumentTitleTransaction' => 'applications/phriction/xaction/PhrictionDocumentTitleTransaction.php',
'PhrictionDocumentTransactionType' => 'applications/phriction/xaction/PhrictionDocumentTransactionType.php',
'PhrictionDocumentVersionTransaction' => 'applications/phriction/xaction/PhrictionDocumentVersionTransaction.php',
'PhrictionEditConduitAPIMethod' => 'applications/phriction/conduit/PhrictionEditConduitAPIMethod.php',
'PhrictionEditController' => 'applications/phriction/controller/PhrictionEditController.php',
'PhrictionEditEngineController' => 'applications/phriction/controller/PhrictionEditEngineController.php',
'PhrictionHistoryConduitAPIMethod' => 'applications/phriction/conduit/PhrictionHistoryConduitAPIMethod.php',
'PhrictionHistoryController' => 'applications/phriction/controller/PhrictionHistoryController.php',
'PhrictionInfoConduitAPIMethod' => 'applications/phriction/conduit/PhrictionInfoConduitAPIMethod.php',
'PhrictionListController' => 'applications/phriction/controller/PhrictionListController.php',
'PhrictionMarkupPreviewController' => 'applications/phriction/controller/PhrictionMarkupPreviewController.php',
'PhrictionMoveController' => 'applications/phriction/controller/PhrictionMoveController.php',
'PhrictionNewController' => 'applications/phriction/controller/PhrictionNewController.php',
'PhrictionPublishController' => 'applications/phriction/controller/PhrictionPublishController.php',
'PhrictionRemarkupRule' => 'applications/phriction/markup/PhrictionRemarkupRule.php',
'PhrictionReplyHandler' => 'applications/phriction/mail/PhrictionReplyHandler.php',
'PhrictionSchemaSpec' => 'applications/phriction/storage/PhrictionSchemaSpec.php',
'PhrictionTransaction' => 'applications/phriction/storage/PhrictionTransaction.php',
'PhrictionTransactionComment' => 'applications/phriction/storage/PhrictionTransactionComment.php',
'PhrictionTransactionEditor' => 'applications/phriction/editor/PhrictionTransactionEditor.php',
'PhrictionTransactionQuery' => 'applications/phriction/query/PhrictionTransactionQuery.php',
'PolicyLockOptionType' => 'applications/policy/config/PolicyLockOptionType.php',
'PonderAddAnswerView' => 'applications/ponder/view/PonderAddAnswerView.php',
'PonderAnswer' => 'applications/ponder/storage/PonderAnswer.php',
'PonderAnswerCommentController' => 'applications/ponder/controller/PonderAnswerCommentController.php',
'PonderAnswerContentTransaction' => 'applications/ponder/xaction/PonderAnswerContentTransaction.php',
'PonderAnswerEditController' => 'applications/ponder/controller/PonderAnswerEditController.php',
'PonderAnswerEditor' => 'applications/ponder/editor/PonderAnswerEditor.php',
'PonderAnswerHistoryController' => 'applications/ponder/controller/PonderAnswerHistoryController.php',
'PonderAnswerMailReceiver' => 'applications/ponder/mail/PonderAnswerMailReceiver.php',
'PonderAnswerPHIDType' => 'applications/ponder/phid/PonderAnswerPHIDType.php',
'PonderAnswerQuery' => 'applications/ponder/query/PonderAnswerQuery.php',
'PonderAnswerQuestionIDTransaction' => 'applications/ponder/xaction/PonderAnswerQuestionIDTransaction.php',
'PonderAnswerReplyHandler' => 'applications/ponder/mail/PonderAnswerReplyHandler.php',
'PonderAnswerSaveController' => 'applications/ponder/controller/PonderAnswerSaveController.php',
'PonderAnswerStatus' => 'applications/ponder/constants/PonderAnswerStatus.php',
'PonderAnswerStatusTransaction' => 'applications/ponder/xaction/PonderAnswerStatusTransaction.php',
'PonderAnswerTransaction' => 'applications/ponder/storage/PonderAnswerTransaction.php',
'PonderAnswerTransactionComment' => 'applications/ponder/storage/PonderAnswerTransactionComment.php',
'PonderAnswerTransactionQuery' => 'applications/ponder/query/PonderAnswerTransactionQuery.php',
'PonderAnswerTransactionType' => 'applications/ponder/xaction/PonderAnswerTransactionType.php',
'PonderAnswerView' => 'applications/ponder/view/PonderAnswerView.php',
'PonderConstants' => 'applications/ponder/constants/PonderConstants.php',
'PonderController' => 'applications/ponder/controller/PonderController.php',
'PonderDAO' => 'applications/ponder/storage/PonderDAO.php',
'PonderDefaultViewCapability' => 'applications/ponder/capability/PonderDefaultViewCapability.php',
'PonderEditor' => 'applications/ponder/editor/PonderEditor.php',
'PonderFooterView' => 'applications/ponder/view/PonderFooterView.php',
'PonderModerateCapability' => 'applications/ponder/capability/PonderModerateCapability.php',
'PonderQuestion' => 'applications/ponder/storage/PonderQuestion.php',
'PonderQuestionAnswerTransaction' => 'applications/ponder/xaction/PonderQuestionAnswerTransaction.php',
'PonderQuestionAnswerWikiTransaction' => 'applications/ponder/xaction/PonderQuestionAnswerWikiTransaction.php',
'PonderQuestionCommentController' => 'applications/ponder/controller/PonderQuestionCommentController.php',
'PonderQuestionContentTransaction' => 'applications/ponder/xaction/PonderQuestionContentTransaction.php',
'PonderQuestionCreateMailReceiver' => 'applications/ponder/mail/PonderQuestionCreateMailReceiver.php',
'PonderQuestionEditController' => 'applications/ponder/controller/PonderQuestionEditController.php',
'PonderQuestionEditEngine' => 'applications/ponder/editor/PonderQuestionEditEngine.php',
'PonderQuestionEditor' => 'applications/ponder/editor/PonderQuestionEditor.php',
'PonderQuestionFerretEngine' => 'applications/ponder/search/PonderQuestionFerretEngine.php',
'PonderQuestionFulltextEngine' => 'applications/ponder/search/PonderQuestionFulltextEngine.php',
'PonderQuestionHistoryController' => 'applications/ponder/controller/PonderQuestionHistoryController.php',
'PonderQuestionListController' => 'applications/ponder/controller/PonderQuestionListController.php',
'PonderQuestionMailReceiver' => 'applications/ponder/mail/PonderQuestionMailReceiver.php',
'PonderQuestionPHIDType' => 'applications/ponder/phid/PonderQuestionPHIDType.php',
'PonderQuestionQuery' => 'applications/ponder/query/PonderQuestionQuery.php',
'PonderQuestionReplyHandler' => 'applications/ponder/mail/PonderQuestionReplyHandler.php',
'PonderQuestionSearchEngine' => 'applications/ponder/query/PonderQuestionSearchEngine.php',
'PonderQuestionStatus' => 'applications/ponder/constants/PonderQuestionStatus.php',
'PonderQuestionStatusController' => 'applications/ponder/controller/PonderQuestionStatusController.php',
'PonderQuestionStatusTransaction' => 'applications/ponder/xaction/PonderQuestionStatusTransaction.php',
'PonderQuestionTitleTransaction' => 'applications/ponder/xaction/PonderQuestionTitleTransaction.php',
'PonderQuestionTransaction' => 'applications/ponder/storage/PonderQuestionTransaction.php',
'PonderQuestionTransactionComment' => 'applications/ponder/storage/PonderQuestionTransactionComment.php',
'PonderQuestionTransactionQuery' => 'applications/ponder/query/PonderQuestionTransactionQuery.php',
'PonderQuestionTransactionType' => 'applications/ponder/xaction/PonderQuestionTransactionType.php',
'PonderQuestionViewController' => 'applications/ponder/controller/PonderQuestionViewController.php',
'PonderRemarkupRule' => 'applications/ponder/remarkup/PonderRemarkupRule.php',
'PonderSchemaSpec' => 'applications/ponder/storage/PonderSchemaSpec.php',
'ProjectAddProjectsEmailCommand' => 'applications/project/command/ProjectAddProjectsEmailCommand.php',
'ProjectBoardTaskCard' => 'applications/project/view/ProjectBoardTaskCard.php',
'ProjectCanLockProjectsCapability' => 'applications/project/capability/ProjectCanLockProjectsCapability.php',
'ProjectColumnSearchConduitAPIMethod' => 'applications/project/conduit/ProjectColumnSearchConduitAPIMethod.php',
'ProjectConduitAPIMethod' => 'applications/project/conduit/ProjectConduitAPIMethod.php',
'ProjectCreateConduitAPIMethod' => 'applications/project/conduit/ProjectCreateConduitAPIMethod.php',
'ProjectCreateProjectsCapability' => 'applications/project/capability/ProjectCreateProjectsCapability.php',
'ProjectDatasourceEngineExtension' => 'applications/project/engineextension/ProjectDatasourceEngineExtension.php',
'ProjectDefaultEditCapability' => 'applications/project/capability/ProjectDefaultEditCapability.php',
'ProjectDefaultJoinCapability' => 'applications/project/capability/ProjectDefaultJoinCapability.php',
'ProjectDefaultViewCapability' => 'applications/project/capability/ProjectDefaultViewCapability.php',
'ProjectEditConduitAPIMethod' => 'applications/project/conduit/ProjectEditConduitAPIMethod.php',
'ProjectQueryConduitAPIMethod' => 'applications/project/conduit/ProjectQueryConduitAPIMethod.php',
'ProjectRemarkupRule' => 'applications/project/remarkup/ProjectRemarkupRule.php',
'ProjectRemarkupRuleTestCase' => 'applications/project/remarkup/__tests__/ProjectRemarkupRuleTestCase.php',
'ProjectReplyHandler' => 'applications/project/mail/ProjectReplyHandler.php',
'ProjectSearchConduitAPIMethod' => 'applications/project/conduit/ProjectSearchConduitAPIMethod.php',
'QueryFormattingTestCase' => 'infrastructure/storage/__tests__/QueryFormattingTestCase.php',
'ReleephAuthorFieldSpecification' => 'applications/releeph/field/specification/ReleephAuthorFieldSpecification.php',
'ReleephBranch' => 'applications/releeph/storage/ReleephBranch.php',
'ReleephBranchAccessController' => 'applications/releeph/controller/branch/ReleephBranchAccessController.php',
'ReleephBranchCommitFieldSpecification' => 'applications/releeph/field/specification/ReleephBranchCommitFieldSpecification.php',
'ReleephBranchController' => 'applications/releeph/controller/branch/ReleephBranchController.php',
'ReleephBranchCreateController' => 'applications/releeph/controller/branch/ReleephBranchCreateController.php',
'ReleephBranchEditController' => 'applications/releeph/controller/branch/ReleephBranchEditController.php',
'ReleephBranchEditor' => 'applications/releeph/editor/ReleephBranchEditor.php',
'ReleephBranchHistoryController' => 'applications/releeph/controller/branch/ReleephBranchHistoryController.php',
'ReleephBranchNamePreviewController' => 'applications/releeph/controller/branch/ReleephBranchNamePreviewController.php',
'ReleephBranchPHIDType' => 'applications/releeph/phid/ReleephBranchPHIDType.php',
'ReleephBranchPreviewView' => 'applications/releeph/view/branch/ReleephBranchPreviewView.php',
'ReleephBranchQuery' => 'applications/releeph/query/ReleephBranchQuery.php',
'ReleephBranchSearchEngine' => 'applications/releeph/query/ReleephBranchSearchEngine.php',
'ReleephBranchTemplate' => 'applications/releeph/view/branch/ReleephBranchTemplate.php',
'ReleephBranchTransaction' => 'applications/releeph/storage/ReleephBranchTransaction.php',
'ReleephBranchTransactionQuery' => 'applications/releeph/query/ReleephBranchTransactionQuery.php',
'ReleephBranchViewController' => 'applications/releeph/controller/branch/ReleephBranchViewController.php',
'ReleephCommitFinder' => 'applications/releeph/commitfinder/ReleephCommitFinder.php',
'ReleephCommitFinderException' => 'applications/releeph/commitfinder/ReleephCommitFinderException.php',
'ReleephCommitMessageFieldSpecification' => 'applications/releeph/field/specification/ReleephCommitMessageFieldSpecification.php',
'ReleephConduitAPIMethod' => 'applications/releeph/conduit/ReleephConduitAPIMethod.php',
'ReleephController' => 'applications/releeph/controller/ReleephController.php',
'ReleephDAO' => 'applications/releeph/storage/ReleephDAO.php',
'ReleephDefaultFieldSelector' => 'applications/releeph/field/selector/ReleephDefaultFieldSelector.php',
'ReleephDependsOnFieldSpecification' => 'applications/releeph/field/specification/ReleephDependsOnFieldSpecification.php',
'ReleephDiffChurnFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffChurnFieldSpecification.php',
'ReleephDiffMessageFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffMessageFieldSpecification.php',
'ReleephDiffSizeFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffSizeFieldSpecification.php',
'ReleephFieldParseException' => 'applications/releeph/field/exception/ReleephFieldParseException.php',
'ReleephFieldSelector' => 'applications/releeph/field/selector/ReleephFieldSelector.php',
'ReleephFieldSpecification' => 'applications/releeph/field/specification/ReleephFieldSpecification.php',
'ReleephGetBranchesConduitAPIMethod' => 'applications/releeph/conduit/ReleephGetBranchesConduitAPIMethod.php',
'ReleephIntentFieldSpecification' => 'applications/releeph/field/specification/ReleephIntentFieldSpecification.php',
'ReleephLevelFieldSpecification' => 'applications/releeph/field/specification/ReleephLevelFieldSpecification.php',
'ReleephOriginalCommitFieldSpecification' => 'applications/releeph/field/specification/ReleephOriginalCommitFieldSpecification.php',
'ReleephProductActionController' => 'applications/releeph/controller/product/ReleephProductActionController.php',
'ReleephProductController' => 'applications/releeph/controller/product/ReleephProductController.php',
'ReleephProductCreateController' => 'applications/releeph/controller/product/ReleephProductCreateController.php',
'ReleephProductEditController' => 'applications/releeph/controller/product/ReleephProductEditController.php',
'ReleephProductEditor' => 'applications/releeph/editor/ReleephProductEditor.php',
'ReleephProductHistoryController' => 'applications/releeph/controller/product/ReleephProductHistoryController.php',
'ReleephProductListController' => 'applications/releeph/controller/product/ReleephProductListController.php',
'ReleephProductPHIDType' => 'applications/releeph/phid/ReleephProductPHIDType.php',
'ReleephProductQuery' => 'applications/releeph/query/ReleephProductQuery.php',
'ReleephProductSearchEngine' => 'applications/releeph/query/ReleephProductSearchEngine.php',
'ReleephProductTransaction' => 'applications/releeph/storage/ReleephProductTransaction.php',
'ReleephProductTransactionQuery' => 'applications/releeph/query/ReleephProductTransactionQuery.php',
'ReleephProductViewController' => 'applications/releeph/controller/product/ReleephProductViewController.php',
'ReleephProject' => 'applications/releeph/storage/ReleephProject.php',
'ReleephQueryBranchesConduitAPIMethod' => 'applications/releeph/conduit/ReleephQueryBranchesConduitAPIMethod.php',
'ReleephQueryProductsConduitAPIMethod' => 'applications/releeph/conduit/ReleephQueryProductsConduitAPIMethod.php',
'ReleephQueryRequestsConduitAPIMethod' => 'applications/releeph/conduit/ReleephQueryRequestsConduitAPIMethod.php',
'ReleephReasonFieldSpecification' => 'applications/releeph/field/specification/ReleephReasonFieldSpecification.php',
'ReleephRequest' => 'applications/releeph/storage/ReleephRequest.php',
'ReleephRequestActionController' => 'applications/releeph/controller/request/ReleephRequestActionController.php',
'ReleephRequestCommentController' => 'applications/releeph/controller/request/ReleephRequestCommentController.php',
'ReleephRequestConduitAPIMethod' => 'applications/releeph/conduit/ReleephRequestConduitAPIMethod.php',
'ReleephRequestController' => 'applications/releeph/controller/request/ReleephRequestController.php',
'ReleephRequestDifferentialCreateController' => 'applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php',
'ReleephRequestEditController' => 'applications/releeph/controller/request/ReleephRequestEditController.php',
'ReleephRequestMailReceiver' => 'applications/releeph/mail/ReleephRequestMailReceiver.php',
'ReleephRequestPHIDType' => 'applications/releeph/phid/ReleephRequestPHIDType.php',
'ReleephRequestQuery' => 'applications/releeph/query/ReleephRequestQuery.php',
'ReleephRequestReplyHandler' => 'applications/releeph/mail/ReleephRequestReplyHandler.php',
'ReleephRequestSearchEngine' => 'applications/releeph/query/ReleephRequestSearchEngine.php',
'ReleephRequestStatus' => 'applications/releeph/constants/ReleephRequestStatus.php',
'ReleephRequestTransaction' => 'applications/releeph/storage/ReleephRequestTransaction.php',
'ReleephRequestTransactionComment' => 'applications/releeph/storage/ReleephRequestTransactionComment.php',
'ReleephRequestTransactionQuery' => 'applications/releeph/query/ReleephRequestTransactionQuery.php',
'ReleephRequestTransactionalEditor' => 'applications/releeph/editor/ReleephRequestTransactionalEditor.php',
'ReleephRequestTypeaheadControl' => 'applications/releeph/view/request/ReleephRequestTypeaheadControl.php',
'ReleephRequestTypeaheadController' => 'applications/releeph/controller/request/ReleephRequestTypeaheadController.php',
'ReleephRequestView' => 'applications/releeph/view/ReleephRequestView.php',
'ReleephRequestViewController' => 'applications/releeph/controller/request/ReleephRequestViewController.php',
'ReleephRequestorFieldSpecification' => 'applications/releeph/field/specification/ReleephRequestorFieldSpecification.php',
'ReleephRevisionFieldSpecification' => 'applications/releeph/field/specification/ReleephRevisionFieldSpecification.php',
'ReleephSeverityFieldSpecification' => 'applications/releeph/field/specification/ReleephSeverityFieldSpecification.php',
'ReleephSummaryFieldSpecification' => 'applications/releeph/field/specification/ReleephSummaryFieldSpecification.php',
'ReleephWorkCanPushConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkCanPushConduitAPIMethod.php',
'ReleephWorkGetAuthorInfoConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetAuthorInfoConduitAPIMethod.php',
'ReleephWorkGetBranchCommitMessageConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetBranchCommitMessageConduitAPIMethod.php',
'ReleephWorkGetBranchConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetBranchConduitAPIMethod.php',
'ReleephWorkGetCommitMessageConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetCommitMessageConduitAPIMethod.php',
'ReleephWorkNextRequestConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkNextRequestConduitAPIMethod.php',
'ReleephWorkRecordConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkRecordConduitAPIMethod.php',
'ReleephWorkRecordPickStatusConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkRecordPickStatusConduitAPIMethod.php',
'RemarkupProcessConduitAPIMethod' => 'applications/remarkup/conduit/RemarkupProcessConduitAPIMethod.php',
'RepositoryConduitAPIMethod' => 'applications/repository/conduit/RepositoryConduitAPIMethod.php',
'RepositoryQueryConduitAPIMethod' => 'applications/repository/conduit/RepositoryQueryConduitAPIMethod.php',
'ShellLogView' => 'applications/harbormaster/view/ShellLogView.php',
'SlowvoteConduitAPIMethod' => 'applications/slowvote/conduit/SlowvoteConduitAPIMethod.php',
'SlowvoteEmbedView' => 'applications/slowvote/view/SlowvoteEmbedView.php',
'SlowvoteInfoConduitAPIMethod' => 'applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php',
'SlowvoteRemarkupRule' => 'applications/slowvote/remarkup/SlowvoteRemarkupRule.php',
'SubscriptionListDialogBuilder' => 'applications/subscriptions/view/SubscriptionListDialogBuilder.php',
'SubscriptionListStringBuilder' => 'applications/subscriptions/view/SubscriptionListStringBuilder.php',
'TokenConduitAPIMethod' => 'applications/tokens/conduit/TokenConduitAPIMethod.php',
'TokenGiveConduitAPIMethod' => 'applications/tokens/conduit/TokenGiveConduitAPIMethod.php',
'TokenGivenConduitAPIMethod' => 'applications/tokens/conduit/TokenGivenConduitAPIMethod.php',
'TokenQueryConduitAPIMethod' => 'applications/tokens/conduit/TokenQueryConduitAPIMethod.php',
'TransactionSearchConduitAPIMethod' => 'applications/transactions/conduit/TransactionSearchConduitAPIMethod.php',
'UserConduitAPIMethod' => 'applications/people/conduit/UserConduitAPIMethod.php',
'UserDisableConduitAPIMethod' => 'applications/people/conduit/UserDisableConduitAPIMethod.php',
'UserEditConduitAPIMethod' => 'applications/people/conduit/UserEditConduitAPIMethod.php',
'UserEnableConduitAPIMethod' => 'applications/people/conduit/UserEnableConduitAPIMethod.php',
'UserFindConduitAPIMethod' => 'applications/people/conduit/UserFindConduitAPIMethod.php',
'UserQueryConduitAPIMethod' => 'applications/people/conduit/UserQueryConduitAPIMethod.php',
'UserSearchConduitAPIMethod' => 'applications/people/conduit/UserSearchConduitAPIMethod.php',
'UserWhoAmIConduitAPIMethod' => 'applications/people/conduit/UserWhoAmIConduitAPIMethod.php',
),
'function' => array(
'celerity_generate_unique_node_id' => 'applications/celerity/api.php',
'celerity_get_resource_uri' => 'applications/celerity/api.php',
'javelin_tag' => 'infrastructure/javelin/markup.php',
'phabricator_date' => 'view/viewutils.php',
'phabricator_datetime' => 'view/viewutils.php',
'phabricator_datetimezone' => 'view/viewutils.php',
'phabricator_form' => 'infrastructure/javelin/markup.php',
'phabricator_format_local_time' => 'view/viewutils.php',
'phabricator_relative_date' => 'view/viewutils.php',
'phabricator_time' => 'view/viewutils.php',
'phid_get_subtype' => 'applications/phid/utils.php',
'phid_get_type' => 'applications/phid/utils.php',
'phid_group_by_type' => 'applications/phid/utils.php',
'require_celerity_resource' => 'applications/celerity/api.php',
),
'xmap' => array(
'AlmanacAddress' => 'Phobject',
'AlmanacBinding' => array(
'AlmanacDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'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' => 'AlmanacModularTransaction',
'AlmanacBindingTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'AlmanacBindingTransactionType' => 'AlmanacTransactionType',
'AlmanacBindingViewController' => 'AlmanacServiceController',
'AlmanacBindingsSearchEngineAttachment' => 'AlmanacSearchEngineAttachment',
'AlmanacCacheEngineExtension' => 'PhabricatorCacheEngineExtension',
'AlmanacClusterDatabaseServiceType' => 'AlmanacClusterServiceType',
'AlmanacClusterRepositoryServiceType' => 'AlmanacClusterServiceType',
'AlmanacClusterServiceType' => 'AlmanacServiceType',
'AlmanacConsoleController' => 'AlmanacController',
'AlmanacController' => 'PhabricatorController',
'AlmanacCreateDevicesCapability' => 'PhabricatorPolicyCapability',
'AlmanacCreateNamespacesCapability' => 'PhabricatorPolicyCapability',
'AlmanacCreateNetworksCapability' => 'PhabricatorPolicyCapability',
'AlmanacCreateServicesCapability' => 'PhabricatorPolicyCapability',
'AlmanacCustomServiceType' => 'AlmanacServiceType',
'AlmanacDAO' => 'PhabricatorLiskDAO',
'AlmanacDeletePropertyEditField' => 'PhabricatorEditField',
'AlmanacDeletePropertyEditType' => 'PhabricatorEditType',
'AlmanacDevice' => array(
'AlmanacDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorProjectInterface',
'PhabricatorSSHPublicKeyInterface',
'AlmanacPropertyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorNgramsInterface',
'PhabricatorConduitResultInterface',
'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',
'AlmanacDeviceSetPropertyTransaction' => 'AlmanacDeviceTransactionType',
'AlmanacDeviceTransaction' => 'AlmanacModularTransaction',
'AlmanacDeviceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'AlmanacDeviceTransactionType' => 'AlmanacTransactionType',
'AlmanacDeviceViewController' => 'AlmanacDeviceController',
'AlmanacDrydockPoolServiceType' => 'AlmanacServiceType',
'AlmanacEditor' => 'PhabricatorApplicationTransactionEditor',
'AlmanacInterface' => array(
'AlmanacDAO',
'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(
'AlmanacDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorProjectInterface',
'AlmanacPropertyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorNgramsInterface',
'PhabricatorConduitResultInterface',
),
'AlmanacNamespaceController' => 'AlmanacController',
'AlmanacNamespaceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'AlmanacNamespaceEditController' => 'AlmanacNamespaceController',
'AlmanacNamespaceEditEngine' => 'PhabricatorEditEngine',
'AlmanacNamespaceEditor' => 'AlmanacEditor',
'AlmanacNamespaceListController' => 'AlmanacNamespaceController',
'AlmanacNamespaceNameNgrams' => 'PhabricatorSearchNgrams',
'AlmanacNamespaceNameTransaction' => 'AlmanacNamespaceTransactionType',
'AlmanacNamespacePHIDType' => 'PhabricatorPHIDType',
'AlmanacNamespaceQuery' => 'AlmanacQuery',
'AlmanacNamespaceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'AlmanacNamespaceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'AlmanacNamespaceTransaction' => 'AlmanacModularTransaction',
'AlmanacNamespaceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'AlmanacNamespaceTransactionType' => 'AlmanacTransactionType',
'AlmanacNamespaceViewController' => 'AlmanacNamespaceController',
'AlmanacNetwork' => array(
'AlmanacDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorNgramsInterface',
'PhabricatorConduitResultInterface',
),
'AlmanacNetworkController' => 'AlmanacController',
'AlmanacNetworkEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'AlmanacNetworkEditController' => 'AlmanacNetworkController',
'AlmanacNetworkEditEngine' => 'PhabricatorEditEngine',
'AlmanacNetworkEditor' => 'AlmanacEditor',
'AlmanacNetworkListController' => 'AlmanacNetworkController',
'AlmanacNetworkNameNgrams' => 'PhabricatorSearchNgrams',
'AlmanacNetworkNameTransaction' => 'AlmanacNetworkTransactionType',
'AlmanacNetworkPHIDType' => 'PhabricatorPHIDType',
'AlmanacNetworkQuery' => 'AlmanacQuery',
'AlmanacNetworkSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'AlmanacNetworkSearchEngine' => 'PhabricatorApplicationSearchEngine',
'AlmanacNetworkTransaction' => 'AlmanacModularTransaction',
'AlmanacNetworkTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'AlmanacNetworkTransactionType' => 'AlmanacTransactionType',
'AlmanacNetworkViewController' => 'AlmanacNetworkController',
'AlmanacPropertiesDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'AlmanacPropertiesEditEngineExtension' => 'PhabricatorEditEngineExtension',
'AlmanacPropertiesSearchEngineAttachment' => 'AlmanacSearchEngineAttachment',
'AlmanacProperty' => array(
'AlmanacDAO',
'PhabricatorPolicyInterface',
),
'AlmanacPropertyController' => 'AlmanacController',
'AlmanacPropertyDeleteController' => 'AlmanacPropertyController',
'AlmanacPropertyEditController' => 'AlmanacPropertyController',
'AlmanacPropertyEditEngine' => 'PhabricatorEditEngine',
'AlmanacPropertyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'AlmanacQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'AlmanacSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'AlmanacSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'AlmanacService' => array(
'AlmanacDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorProjectInterface',
'AlmanacPropertyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorNgramsInterface',
'PhabricatorConduitResultInterface',
'PhabricatorExtendedPolicyInterface',
),
'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',
'AlmanacServiceSetPropertyTransaction' => 'AlmanacServiceTransactionType',
'AlmanacServiceTransaction' => 'AlmanacModularTransaction',
'AlmanacServiceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'AlmanacServiceTransactionType' => 'AlmanacTransactionType',
'AlmanacServiceType' => 'Phobject',
'AlmanacServiceTypeDatasource' => 'PhabricatorTypeaheadDatasource',
'AlmanacServiceTypeTestCase' => 'PhabricatorTestCase',
'AlmanacServiceTypeTransaction' => 'AlmanacServiceTransactionType',
'AlmanacServiceViewController' => 'AlmanacServiceController',
'AlmanacSetPropertyEditField' => 'PhabricatorEditField',
'AlmanacSetPropertyEditType' => 'PhabricatorEditType',
'AlmanacTransactionType' => 'PhabricatorModularTransactionType',
'AphlictDropdownDataQuery' => 'Phobject',
'Aphront304Response' => 'AphrontResponse',
'Aphront400Response' => 'AphrontResponse',
'Aphront403Response' => 'AphrontHTMLResponse',
'Aphront404Response' => 'AphrontHTMLResponse',
'AphrontAjaxResponse' => 'AphrontResponse',
'AphrontApplicationConfiguration' => 'Phobject',
'AphrontBarView' => 'AphrontView',
'AphrontBoolHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontCalendarEventView' => 'AphrontView',
'AphrontController' => 'Phobject',
'AphrontCursorPagerView' => 'AphrontView',
'AphrontDialogResponse' => 'AphrontResponse',
'AphrontDialogView' => array(
'AphrontView',
'AphrontResponseProducerInterface',
),
'AphrontEpochHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontException' => 'Exception',
'AphrontFileHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontFileResponse' => 'AphrontResponse',
'AphrontFormCheckboxControl' => 'AphrontFormControl',
'AphrontFormControl' => 'AphrontView',
'AphrontFormDateControl' => 'AphrontFormControl',
'AphrontFormDateControlValue' => 'Phobject',
'AphrontFormDividerControl' => 'AphrontFormControl',
'AphrontFormFileControl' => 'AphrontFormControl',
'AphrontFormHandlesControl' => 'AphrontFormControl',
'AphrontFormMarkupControl' => 'AphrontFormControl',
'AphrontFormPasswordControl' => 'AphrontFormControl',
'AphrontFormPolicyControl' => 'AphrontFormControl',
'AphrontFormRadioButtonControl' => 'AphrontFormControl',
'AphrontFormRecaptchaControl' => 'AphrontFormControl',
'AphrontFormSelectControl' => 'AphrontFormControl',
'AphrontFormStaticControl' => 'AphrontFormControl',
'AphrontFormSubmitControl' => 'AphrontFormControl',
'AphrontFormTextAreaControl' => 'AphrontFormControl',
'AphrontFormTextControl' => 'AphrontFormControl',
'AphrontFormTextWithSubmitControl' => 'AphrontFormControl',
'AphrontFormTokenizerControl' => 'AphrontFormControl',
'AphrontFormTypeaheadControl' => 'AphrontFormControl',
'AphrontFormView' => 'AphrontView',
'AphrontGlyphBarView' => 'AphrontBarView',
'AphrontHTMLResponse' => 'AphrontResponse',
'AphrontHTTPParameterType' => 'Phobject',
'AphrontHTTPProxyResponse' => 'AphrontResponse',
'AphrontHTTPSink' => 'Phobject',
'AphrontHTTPSinkTestCase' => 'PhabricatorTestCase',
'AphrontIntHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontIsolatedDatabaseConnectionTestCase' => 'PhabricatorTestCase',
'AphrontIsolatedHTTPSink' => 'AphrontHTTPSink',
'AphrontJSONResponse' => 'AphrontResponse',
'AphrontJavelinView' => 'AphrontView',
'AphrontKeyboardShortcutsAvailableView' => 'AphrontView',
'AphrontListFilterView' => 'AphrontView',
'AphrontListHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontMalformedRequestException' => 'AphrontException',
'AphrontMoreView' => 'AphrontView',
'AphrontMultiColumnView' => 'AphrontView',
'AphrontMySQLDatabaseConnectionTestCase' => 'PhabricatorTestCase',
'AphrontNullView' => 'AphrontView',
'AphrontPHIDHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontPHIDListHTTPParameterType' => 'AphrontListHTTPParameterType',
'AphrontPHPHTTPSink' => 'AphrontHTTPSink',
'AphrontPageView' => 'AphrontView',
'AphrontPlainTextResponse' => 'AphrontResponse',
'AphrontProgressBarView' => 'AphrontBarView',
'AphrontProjectListHTTPParameterType' => 'AphrontListHTTPParameterType',
'AphrontProxyResponse' => array(
'AphrontResponse',
'AphrontResponseProducerInterface',
),
'AphrontRedirectResponse' => 'AphrontResponse',
'AphrontRedirectResponseTestCase' => 'PhabricatorTestCase',
'AphrontReloadResponse' => 'AphrontRedirectResponse',
'AphrontRequest' => 'Phobject',
'AphrontRequestExceptionHandler' => 'Phobject',
'AphrontRequestTestCase' => 'PhabricatorTestCase',
'AphrontResponse' => 'Phobject',
'AphrontRoutingMap' => 'Phobject',
'AphrontRoutingResult' => 'Phobject',
'AphrontSelectHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontSideNavFilterView' => 'AphrontView',
'AphrontSite' => 'Phobject',
'AphrontStackTraceView' => 'AphrontView',
'AphrontStandaloneHTMLResponse' => 'AphrontHTMLResponse',
'AphrontStringHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontStringListHTTPParameterType' => 'AphrontListHTTPParameterType',
'AphrontTableView' => 'AphrontView',
'AphrontTagView' => 'AphrontView',
'AphrontTokenizerTemplateView' => 'AphrontView',
'AphrontTypeaheadTemplateView' => 'AphrontView',
'AphrontUnhandledExceptionResponse' => 'AphrontStandaloneHTMLResponse',
'AphrontUserListHTTPParameterType' => 'AphrontListHTTPParameterType',
'AphrontView' => array(
'Phobject',
'PhutilSafeHTMLProducerInterface',
),
'AphrontWebpageResponse' => 'AphrontHTMLResponse',
'ArcanistConduitAPIMethod' => 'ConduitAPIMethod',
'AuditConduitAPIMethod' => 'ConduitAPIMethod',
'AuditQueryConduitAPIMethod' => 'AuditConduitAPIMethod',
'AuthManageProvidersCapability' => 'PhabricatorPolicyCapability',
'BulkParameterType' => 'Phobject',
'BulkPointsParameterType' => 'BulkParameterType',
'BulkRemarkupParameterType' => 'BulkParameterType',
'BulkSelectParameterType' => 'BulkParameterType',
'BulkStringParameterType' => 'BulkParameterType',
'BulkTokenizerParameterType' => 'BulkParameterType',
'CalendarTimeUtil' => 'Phobject',
'CalendarTimeUtilTestCase' => 'PhabricatorTestCase',
'CelerityAPI' => 'Phobject',
'CelerityDarkModePostprocessor' => 'CelerityPostprocessor',
'CelerityDefaultPostprocessor' => 'CelerityPostprocessor',
'CelerityHighContrastPostprocessor' => 'CelerityPostprocessor',
'CelerityLargeFontPostprocessor' => 'CelerityPostprocessor',
'CelerityManagementMapWorkflow' => 'CelerityManagementWorkflow',
'CelerityManagementSyntaxWorkflow' => 'CelerityManagementWorkflow',
'CelerityManagementWorkflow' => 'PhabricatorManagementWorkflow',
'CelerityPhabricatorResourceController' => 'CelerityResourceController',
'CelerityPhabricatorResources' => 'CelerityResourcesOnDisk',
'CelerityPhysicalResources' => 'CelerityResources',
'CelerityPhysicalResourcesTestCase' => 'PhabricatorTestCase',
'CelerityPostprocessor' => 'Phobject',
'CelerityPostprocessorTestCase' => 'PhabricatorTestCase',
'CelerityRedGreenPostprocessor' => 'CelerityPostprocessor',
'CelerityResourceController' => 'PhabricatorController',
'CelerityResourceGraph' => 'AbstractDirectedGraph',
'CelerityResourceMap' => 'Phobject',
'CelerityResourceMapGenerator' => 'Phobject',
'CelerityResourceTransformer' => 'Phobject',
'CelerityResourceTransformerTestCase' => 'PhabricatorTestCase',
'CelerityResources' => 'Phobject',
'CelerityResourcesOnDisk' => 'CelerityPhysicalResources',
'CeleritySpriteGenerator' => 'Phobject',
'CelerityStaticResourceResponse' => 'Phobject',
'ChatLogConduitAPIMethod' => 'ConduitAPIMethod',
'ChatLogQueryConduitAPIMethod' => 'ChatLogConduitAPIMethod',
'ChatLogRecordConduitAPIMethod' => 'ChatLogConduitAPIMethod',
'ConduitAPIMethod' => array(
'Phobject',
'PhabricatorPolicyInterface',
),
'ConduitAPIMethodTestCase' => 'PhabricatorTestCase',
'ConduitAPIRequest' => 'Phobject',
'ConduitAPIResponse' => 'Phobject',
'ConduitApplicationNotInstalledException' => 'ConduitMethodNotFoundException',
'ConduitBoolParameterType' => 'ConduitParameterType',
'ConduitCall' => 'Phobject',
'ConduitCallTestCase' => 'PhabricatorTestCase',
'ConduitColumnsParameterType' => 'ConduitParameterType',
'ConduitConnectConduitAPIMethod' => 'ConduitAPIMethod',
'ConduitConstantDescription' => 'Phobject',
'ConduitEpochParameterType' => 'ConduitParameterType',
'ConduitException' => 'Exception',
'ConduitGetCapabilitiesConduitAPIMethod' => 'ConduitAPIMethod',
'ConduitGetCertificateConduitAPIMethod' => 'ConduitAPIMethod',
'ConduitIntListParameterType' => 'ConduitListParameterType',
'ConduitIntParameterType' => 'ConduitParameterType',
'ConduitListParameterType' => 'ConduitParameterType',
'ConduitLogGarbageCollector' => 'PhabricatorGarbageCollector',
'ConduitMethodDoesNotExistException' => 'ConduitMethodNotFoundException',
'ConduitMethodNotFoundException' => 'ConduitException',
'ConduitPHIDListParameterType' => 'ConduitListParameterType',
'ConduitPHIDParameterType' => 'ConduitParameterType',
'ConduitParameterType' => 'Phobject',
'ConduitPingConduitAPIMethod' => 'ConduitAPIMethod',
'ConduitPointsParameterType' => 'ConduitParameterType',
'ConduitProjectListParameterType' => 'ConduitListParameterType',
'ConduitQueryConduitAPIMethod' => 'ConduitAPIMethod',
'ConduitResultSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'ConduitSSHWorkflow' => 'PhabricatorSSHWorkflow',
'ConduitStringListParameterType' => 'ConduitListParameterType',
'ConduitStringParameterType' => 'ConduitParameterType',
'ConduitTokenGarbageCollector' => 'PhabricatorGarbageCollector',
'ConduitUserListParameterType' => 'ConduitListParameterType',
'ConduitUserParameterType' => 'ConduitParameterType',
'ConduitWildParameterType' => 'ConduitParameterType',
'ConpherenceColumnViewController' => 'ConpherenceController',
'ConpherenceConduitAPIMethod' => 'ConduitAPIMethod',
'ConpherenceConstants' => 'Phobject',
'ConpherenceController' => 'PhabricatorController',
'ConpherenceCreateThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod',
'ConpherenceDAO' => 'PhabricatorLiskDAO',
'ConpherenceDurableColumnView' => 'AphrontTagView',
'ConpherenceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'ConpherenceEditEngine' => 'PhabricatorEditEngine',
'ConpherenceEditor' => 'PhabricatorApplicationTransactionEditor',
'ConpherenceFulltextQuery' => 'PhabricatorOffsetPagedQuery',
'ConpherenceIndex' => 'ConpherenceDAO',
'ConpherenceLayoutView' => 'AphrontTagView',
'ConpherenceListController' => 'ConpherenceController',
'ConpherenceMenuItemView' => 'AphrontTagView',
'ConpherenceNotificationPanelController' => 'ConpherenceController',
'ConpherenceParticipant' => 'ConpherenceDAO',
'ConpherenceParticipantController' => 'ConpherenceController',
'ConpherenceParticipantCountQuery' => 'PhabricatorOffsetPagedQuery',
'ConpherenceParticipantQuery' => 'PhabricatorOffsetPagedQuery',
'ConpherenceParticipantView' => 'AphrontView',
'ConpherenceQueryThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod',
'ConpherenceQueryTransactionConduitAPIMethod' => 'ConpherenceConduitAPIMethod',
'ConpherenceReplyHandler' => 'PhabricatorMailReplyHandler',
'ConpherenceRoomEditController' => 'ConpherenceController',
'ConpherenceRoomListController' => 'ConpherenceController',
'ConpherenceRoomPictureController' => 'ConpherenceController',
'ConpherenceRoomPreferencesController' => 'ConpherenceController',
'ConpherenceRoomSettings' => 'ConpherenceConstants',
'ConpherenceRoomTestCase' => 'ConpherenceTestCase',
'ConpherenceSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'ConpherenceTestCase' => 'PhabricatorTestCase',
'ConpherenceThread' => array(
'ConpherenceDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorMentionableInterface',
'PhabricatorDestructibleInterface',
'PhabricatorNgramsInterface',
),
'ConpherenceThreadDatasource' => 'PhabricatorTypeaheadDatasource',
'ConpherenceThreadDateMarkerTransaction' => 'ConpherenceThreadTransactionType',
'ConpherenceThreadIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
'ConpherenceThreadListView' => 'AphrontView',
'ConpherenceThreadMailReceiver' => 'PhabricatorObjectMailReceiver',
'ConpherenceThreadMembersPolicyRule' => 'PhabricatorPolicyRule',
'ConpherenceThreadParticipantsTransaction' => 'ConpherenceThreadTransactionType',
'ConpherenceThreadPictureTransaction' => 'ConpherenceThreadTransactionType',
'ConpherenceThreadQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'ConpherenceThreadRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'ConpherenceThreadSearchController' => 'ConpherenceController',
'ConpherenceThreadSearchEngine' => 'PhabricatorApplicationSearchEngine',
'ConpherenceThreadTitleNgrams' => 'PhabricatorSearchNgrams',
'ConpherenceThreadTitleTransaction' => 'ConpherenceThreadTransactionType',
'ConpherenceThreadTopicTransaction' => 'ConpherenceThreadTransactionType',
'ConpherenceThreadTransactionType' => 'PhabricatorModularTransactionType',
'ConpherenceTransaction' => 'PhabricatorModularTransaction',
'ConpherenceTransactionComment' => 'PhabricatorApplicationTransactionComment',
'ConpherenceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'ConpherenceTransactionRenderer' => 'Phobject',
'ConpherenceTransactionView' => 'AphrontView',
'ConpherenceUpdateActions' => 'ConpherenceConstants',
'ConpherenceUpdateController' => 'ConpherenceController',
'ConpherenceUpdateThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod',
'ConpherenceViewController' => 'ConpherenceController',
'CountdownEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'CountdownSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DarkConsoleController' => 'PhabricatorController',
'DarkConsoleCore' => 'Phobject',
'DarkConsoleDataController' => 'PhabricatorController',
'DarkConsoleErrorLogPlugin' => 'DarkConsolePlugin',
'DarkConsoleErrorLogPluginAPI' => 'Phobject',
'DarkConsoleEventPlugin' => 'DarkConsolePlugin',
'DarkConsoleEventPluginAPI' => 'PhabricatorEventListener',
'DarkConsolePlugin' => 'Phobject',
'DarkConsoleRealtimePlugin' => 'DarkConsolePlugin',
'DarkConsoleRequestPlugin' => 'DarkConsolePlugin',
'DarkConsoleServicesPlugin' => 'DarkConsolePlugin',
'DarkConsoleStartupPlugin' => 'DarkConsolePlugin',
'DarkConsoleXHProfPlugin' => 'DarkConsolePlugin',
'DarkConsoleXHProfPluginAPI' => 'Phobject',
'DifferentialAction' => 'Phobject',
'DifferentialActionEmailCommand' => 'MetaMTAEmailTransactionCommand',
'DifferentialAdjustmentMapTestCase' => 'PhutilTestCase',
'DifferentialAffectedPath' => 'DifferentialDAO',
'DifferentialAsanaRepresentationField' => 'DifferentialCustomField',
'DifferentialAuditorsCommitMessageField' => 'DifferentialCommitMessageCustomField',
'DifferentialAuditorsField' => 'DifferentialStoredCustomField',
'DifferentialBlameRevisionCommitMessageField' => 'DifferentialCommitMessageCustomField',
'DifferentialBlameRevisionField' => 'DifferentialStoredCustomField',
'DifferentialBlockHeraldAction' => 'HeraldAction',
'DifferentialBlockingReviewerDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialBranchField' => 'DifferentialCustomField',
'DifferentialBuildableEngine' => 'HarbormasterBuildableEngine',
'DifferentialChangeDetailMailView' => 'DifferentialMailView',
'DifferentialChangeHeraldFieldGroup' => 'HeraldFieldGroup',
'DifferentialChangeType' => 'Phobject',
'DifferentialChangesSinceLastUpdateField' => 'DifferentialCustomField',
'DifferentialChangeset' => array(
'DifferentialDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'DifferentialChangesetDetailView' => 'AphrontView',
'DifferentialChangesetEngine' => 'Phobject',
'DifferentialChangesetFileTreeSideNavBuilder' => 'Phobject',
'DifferentialChangesetHTMLRenderer' => 'DifferentialChangesetRenderer',
'DifferentialChangesetListController' => 'DifferentialController',
'DifferentialChangesetListView' => 'AphrontView',
'DifferentialChangesetOneUpMailRenderer' => 'DifferentialChangesetRenderer',
'DifferentialChangesetOneUpRenderer' => 'DifferentialChangesetHTMLRenderer',
'DifferentialChangesetOneUpTestRenderer' => 'DifferentialChangesetTestRenderer',
'DifferentialChangesetParser' => 'Phobject',
'DifferentialChangesetParserTestCase' => 'PhabricatorTestCase',
'DifferentialChangesetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DifferentialChangesetRenderer' => 'Phobject',
'DifferentialChangesetSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DifferentialChangesetTestRenderer' => 'DifferentialChangesetRenderer',
'DifferentialChangesetTwoUpRenderer' => 'DifferentialChangesetHTMLRenderer',
'DifferentialChangesetTwoUpTestRenderer' => 'DifferentialChangesetTestRenderer',
'DifferentialChangesetViewController' => 'DifferentialController',
'DifferentialCloseConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialCommitMessageCustomField' => 'DifferentialCommitMessageField',
'DifferentialCommitMessageField' => 'Phobject',
'DifferentialCommitMessageFieldTestCase' => 'PhabricatorTestCase',
'DifferentialCommitMessageParser' => 'Phobject',
'DifferentialCommitMessageParserTestCase' => 'PhabricatorTestCase',
'DifferentialCommitsField' => 'DifferentialCustomField',
'DifferentialCommitsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'DifferentialConduitAPIMethod' => 'ConduitAPIMethod',
'DifferentialConflictsCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialController' => 'PhabricatorController',
'DifferentialCoreCustomField' => 'DifferentialCustomField',
'DifferentialCreateCommentConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialCreateDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialCreateInlineConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialCreateMailReceiver' => 'PhabricatorApplicationMailReceiver',
'DifferentialCreateRawDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialCreateRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialCustomField' => 'PhabricatorCustomField',
'DifferentialCustomFieldDependsOnParser' => 'PhabricatorCustomFieldMonogramParser',
'DifferentialCustomFieldDependsOnParserTestCase' => 'PhabricatorTestCase',
'DifferentialCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
'DifferentialCustomFieldRevertsParser' => 'PhabricatorCustomFieldMonogramParser',
'DifferentialCustomFieldRevertsParserTestCase' => 'PhabricatorTestCase',
'DifferentialCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
'DifferentialCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
'DifferentialDAO' => 'PhabricatorLiskDAO',
'DifferentialDefaultViewCapability' => 'PhabricatorPolicyCapability',
'DifferentialDiff' => array(
'DifferentialDAO',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'HarbormasterBuildableInterface',
'HarbormasterCircleCIBuildableInterface',
'HarbormasterBuildkiteBuildableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
),
'DifferentialDiffAffectedFilesHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffAuthorHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffAuthorProjectsHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffContentAddedHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffContentHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffContentRemovedHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffCreateController' => 'DifferentialController',
'DifferentialDiffEditor' => 'PhabricatorApplicationTransactionEditor',
'DifferentialDiffExtractionEngine' => 'Phobject',
'DifferentialDiffHeraldField' => 'HeraldField',
'DifferentialDiffHeraldFieldGroup' => 'HeraldFieldGroup',
'DifferentialDiffInlineCommentQuery' => 'PhabricatorDiffInlineCommentQuery',
'DifferentialDiffPHIDType' => 'PhabricatorPHIDType',
'DifferentialDiffProperty' => 'DifferentialDAO',
'DifferentialDiffQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DifferentialDiffRepositoryHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffRepositoryProjectsHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DifferentialDiffSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DifferentialDiffTestCase' => 'PhutilTestCase',
'DifferentialDiffTransaction' => 'PhabricatorApplicationTransaction',
'DifferentialDiffTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'DifferentialDiffViewController' => 'DifferentialController',
'DifferentialDoorkeeperRevisionFeedStoryPublisher' => 'DoorkeeperFeedStoryPublisher',
'DifferentialDraftField' => 'DifferentialCoreCustomField',
'DifferentialExactUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialFieldParseException' => 'Exception',
'DifferentialFieldValidationException' => 'Exception',
'DifferentialGetAllDiffsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetCommitMessageConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetCommitPathsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetRawDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetRevisionCommentsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetWorkingCopy' => 'Phobject',
'DifferentialGitSVNIDCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialHarbormasterField' => 'DifferentialCustomField',
'DifferentialHeraldStateReasons' => 'HeraldStateReasons',
'DifferentialHiddenComment' => 'DifferentialDAO',
'DifferentialHostField' => 'DifferentialCustomField',
'DifferentialHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension',
'DifferentialHunk' => array(
'DifferentialDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'DifferentialHunkParser' => 'Phobject',
'DifferentialHunkParserTestCase' => 'PhabricatorTestCase',
'DifferentialHunkQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DifferentialHunkTestCase' => 'PhutilTestCase',
'DifferentialInlineComment' => array(
'Phobject',
'PhabricatorInlineCommentInterface',
),
'DifferentialInlineCommentEditController' => 'PhabricatorInlineCommentController',
'DifferentialInlineCommentMailView' => 'DifferentialMailView',
'DifferentialInlineCommentQuery' => 'PhabricatorOffsetPagedQuery',
'DifferentialJIRAIssuesCommitMessageField' => 'DifferentialCommitMessageCustomField',
'DifferentialJIRAIssuesField' => 'DifferentialStoredCustomField',
'DifferentialLegacyQuery' => 'Phobject',
'DifferentialLineAdjustmentMap' => 'Phobject',
'DifferentialLintField' => 'DifferentialHarbormasterField',
'DifferentialLintStatus' => 'Phobject',
'DifferentialLocalCommitsView' => 'AphrontView',
'DifferentialMailEngineExtension' => 'PhabricatorMailEngineExtension',
'DifferentialMailView' => 'Phobject',
'DifferentialManiphestTasksField' => 'DifferentialCoreCustomField',
'DifferentialParseCacheGarbageCollector' => 'PhabricatorGarbageCollector',
'DifferentialParseCommitMessageConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialParseRenderTestCase' => 'PhabricatorTestCase',
'DifferentialPathField' => 'DifferentialCustomField',
'DifferentialProjectReviewersField' => 'DifferentialCustomField',
'DifferentialQueryConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialQueryDiffsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialRawDiffRenderer' => 'Phobject',
'DifferentialReleephRequestFieldSpecification' => 'Phobject',
'DifferentialRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'DifferentialReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'DifferentialRepositoryField' => 'DifferentialCoreCustomField',
'DifferentialRepositoryLookup' => 'Phobject',
'DifferentialRequiredSignaturesField' => 'DifferentialCoreCustomField',
'DifferentialResponsibleDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialResponsibleUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialResponsibleViewerFunctionDatasource' => 'PhabricatorTypeaheadDatasource',
'DifferentialRevertPlanCommitMessageField' => 'DifferentialCommitMessageCustomField',
'DifferentialRevertPlanField' => 'DifferentialStoredCustomField',
'DifferentialReviewedByCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialReviewer' => 'DifferentialDAO',
'DifferentialReviewerDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialReviewerForRevisionEdgeType' => 'PhabricatorEdgeType',
'DifferentialReviewerStatus' => 'Phobject',
'DifferentialReviewersAddBlockingReviewersHeraldAction' => 'DifferentialReviewersHeraldAction',
'DifferentialReviewersAddBlockingSelfHeraldAction' => 'DifferentialReviewersHeraldAction',
'DifferentialReviewersAddReviewersHeraldAction' => 'DifferentialReviewersHeraldAction',
'DifferentialReviewersAddSelfHeraldAction' => 'DifferentialReviewersHeraldAction',
'DifferentialReviewersCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialReviewersField' => 'DifferentialCoreCustomField',
'DifferentialReviewersHeraldAction' => 'HeraldAction',
'DifferentialReviewersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'DifferentialReviewersView' => 'AphrontView',
'DifferentialRevision' => array(
'DifferentialDAO',
'PhabricatorTokenReceiverInterface',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorFlaggableInterface',
'PhrequentTrackableInterface',
'HarbormasterBuildableInterface',
'PhabricatorSubscribableInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorTimelineInterface',
'PhabricatorMentionableInterface',
'PhabricatorDestructibleInterface',
'PhabricatorProjectInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'PhabricatorConduitResultInterface',
'PhabricatorDraftInterface',
),
'DifferentialRevisionAbandonTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionAcceptTransaction' => 'DifferentialRevisionReviewTransaction',
'DifferentialRevisionActionTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionAffectedFilesHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionAuthorHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionAuthorProjectsHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionBuildableTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionCloseDetailsController' => 'DifferentialController',
'DifferentialRevisionCloseTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'DifferentialRevisionCommandeerTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionContentAddedHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionContentHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionContentRemovedHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionControlSystem' => 'Phobject',
'DifferentialRevisionDependedOnByRevisionEdgeType' => 'PhabricatorEdgeType',
'DifferentialRevisionDependsOnRevisionEdgeType' => 'PhabricatorEdgeType',
'DifferentialRevisionDraftEngine' => 'PhabricatorDraftEngine',
'DifferentialRevisionEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'DifferentialRevisionEditController' => 'DifferentialController',
'DifferentialRevisionEditEngine' => 'PhabricatorEditEngine',
'DifferentialRevisionFerretEngine' => 'PhabricatorFerretEngine',
'DifferentialRevisionFulltextEngine' => 'PhabricatorFulltextEngine',
'DifferentialRevisionGraph' => 'PhabricatorObjectGraph',
'DifferentialRevisionHasChildRelationship' => 'DifferentialRevisionRelationship',
'DifferentialRevisionHasCommitEdgeType' => 'PhabricatorEdgeType',
'DifferentialRevisionHasCommitRelationship' => 'DifferentialRevisionRelationship',
'DifferentialRevisionHasParentRelationship' => 'DifferentialRevisionRelationship',
'DifferentialRevisionHasReviewerEdgeType' => 'PhabricatorEdgeType',
'DifferentialRevisionHasTaskEdgeType' => 'PhabricatorEdgeType',
'DifferentialRevisionHasTaskRelationship' => 'DifferentialRevisionRelationship',
'DifferentialRevisionHeraldField' => 'HeraldField',
'DifferentialRevisionHeraldFieldGroup' => 'HeraldFieldGroup',
'DifferentialRevisionHoldDraftTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionIDCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialRevisionInlineTransaction' => 'PhabricatorModularTransactionType',
'DifferentialRevisionInlinesController' => 'DifferentialController',
'DifferentialRevisionListController' => 'DifferentialController',
'DifferentialRevisionListView' => 'AphrontView',
'DifferentialRevisionMailReceiver' => 'PhabricatorObjectMailReceiver',
'DifferentialRevisionOpenStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'DifferentialRevisionOperationController' => 'DifferentialController',
'DifferentialRevisionPHIDType' => 'PhabricatorPHIDType',
'DifferentialRevisionPackageHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionPackageOwnerHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionPlanChangesTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DifferentialRevisionReclaimTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionRejectTransaction' => 'DifferentialRevisionReviewTransaction',
'DifferentialRevisionRelationship' => 'PhabricatorObjectRelationship',
'DifferentialRevisionRelationshipSource' => 'PhabricatorObjectRelationshipSource',
'DifferentialRevisionReopenTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionRepositoryHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionRepositoryProjectsHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionRepositoryTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionRequestReviewTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionRequiredActionResultBucket' => 'DifferentialRevisionResultBucket',
'DifferentialRevisionResignTransaction' => 'DifferentialRevisionReviewTransaction',
'DifferentialRevisionResultBucket' => 'PhabricatorSearchResultBucket',
'DifferentialRevisionReviewTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionReviewersHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionReviewersTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DifferentialRevisionSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DifferentialRevisionStatus' => 'Phobject',
'DifferentialRevisionStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'DifferentialRevisionStatusFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialRevisionStatusHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionStatusTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionSummaryHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionSummaryTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionTestPlanHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionTestPlanTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionTimelineEngine' => 'PhabricatorTimelineEngine',
'DifferentialRevisionTitleHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionTitleTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionTransactionType' => 'PhabricatorModularTransactionType',
'DifferentialRevisionUpdateHistoryView' => 'AphrontView',
'DifferentialRevisionUpdateTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionViewController' => 'DifferentialController',
'DifferentialRevisionVoidTransaction' => 'DifferentialRevisionTransactionType',
+ 'DifferentialRevisionWrongBuildsTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionWrongStateTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'DifferentialSetDiffPropertyConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialStoredCustomField' => 'DifferentialCustomField',
'DifferentialSubscribersCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialSummaryCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialSummaryField' => 'DifferentialCoreCustomField',
'DifferentialTagsCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialTasksCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialTestPlanCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialTestPlanField' => 'DifferentialCoreCustomField',
'DifferentialTitleCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialTransaction' => 'PhabricatorModularTransaction',
'DifferentialTransactionComment' => 'PhabricatorApplicationTransactionComment',
'DifferentialTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'DifferentialTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'DifferentialTransactionView' => 'PhabricatorApplicationTransactionView',
'DifferentialUnitField' => 'DifferentialCustomField',
'DifferentialUnitStatus' => 'Phobject',
'DifferentialUnitTestResult' => 'Phobject',
'DifferentialUpdateRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DiffusionAuditorDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DiffusionAuditorFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DiffusionAuditorsAddAuditorsHeraldAction' => 'DiffusionAuditorsHeraldAction',
'DiffusionAuditorsAddSelfHeraldAction' => 'DiffusionAuditorsHeraldAction',
'DiffusionAuditorsHeraldAction' => 'HeraldAction',
'DiffusionBlameConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionBlameController' => 'DiffusionController',
'DiffusionBlameQuery' => 'DiffusionQuery',
'DiffusionBlockHeraldAction' => 'HeraldAction',
'DiffusionBranchListView' => 'DiffusionView',
'DiffusionBranchQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionBranchTableController' => 'DiffusionController',
'DiffusionBranchTableView' => 'DiffusionView',
'DiffusionBrowseController' => 'DiffusionController',
'DiffusionBrowseQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionBrowseResultSet' => 'Phobject',
'DiffusionBrowseTableView' => 'DiffusionView',
'DiffusionBuildableEngine' => 'HarbormasterBuildableEngine',
'DiffusionCacheEngineExtension' => 'PhabricatorCacheEngineExtension',
'DiffusionCachedResolveRefsQuery' => 'DiffusionLowLevelQuery',
'DiffusionChangeController' => 'DiffusionController',
'DiffusionChangeHeraldFieldGroup' => 'HeraldFieldGroup',
'DiffusionCloneController' => 'DiffusionController',
'DiffusionCloneURIView' => 'AphrontView',
'DiffusionCommandEngine' => 'Phobject',
'DiffusionCommandEngineTestCase' => 'PhabricatorTestCase',
'DiffusionCommitAcceptTransaction' => 'DiffusionCommitAuditTransaction',
'DiffusionCommitActionTransaction' => 'DiffusionCommitTransactionType',
'DiffusionCommitAffectedFilesHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitAuditStatus' => 'Phobject',
'DiffusionCommitAuditTransaction' => 'DiffusionCommitActionTransaction',
'DiffusionCommitAuditorsHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitAuditorsTransaction' => 'DiffusionCommitTransactionType',
'DiffusionCommitAuthorHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitAuthorProjectsHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitAutocloseHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitBranchesController' => 'DiffusionController',
'DiffusionCommitBranchesHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitBuildableTransaction' => 'DiffusionCommitTransactionType',
'DiffusionCommitCommitterHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitCommitterProjectsHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitConcernTransaction' => 'DiffusionCommitAuditTransaction',
'DiffusionCommitController' => 'DiffusionController',
'DiffusionCommitDiffContentAddedHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitDiffContentHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitDiffContentRemovedHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitDiffEnormousHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitDraftEngine' => 'PhabricatorDraftEngine',
'DiffusionCommitEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'DiffusionCommitEditController' => 'DiffusionController',
'DiffusionCommitEditEngine' => 'PhabricatorEditEngine',
'DiffusionCommitFerretEngine' => 'PhabricatorFerretEngine',
'DiffusionCommitFulltextEngine' => 'PhabricatorFulltextEngine',
'DiffusionCommitHasPackageEdgeType' => 'PhabricatorEdgeType',
'DiffusionCommitHasRevisionEdgeType' => 'PhabricatorEdgeType',
'DiffusionCommitHasRevisionRelationship' => 'DiffusionCommitRelationship',
'DiffusionCommitHasTaskEdgeType' => 'PhabricatorEdgeType',
'DiffusionCommitHasTaskRelationship' => 'DiffusionCommitRelationship',
'DiffusionCommitHash' => 'Phobject',
'DiffusionCommitHeraldField' => 'HeraldField',
'DiffusionCommitHeraldFieldGroup' => 'HeraldFieldGroup',
'DiffusionCommitHintQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DiffusionCommitHookEngine' => 'Phobject',
'DiffusionCommitHookRejectException' => 'Exception',
'DiffusionCommitListController' => 'DiffusionController',
'DiffusionCommitListView' => 'AphrontView',
'DiffusionCommitMergeHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitMessageHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitPackageAuditHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitPackageHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitPackageOwnerHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitParentsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionCommitQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DiffusionCommitRef' => 'Phobject',
'DiffusionCommitRelationship' => 'PhabricatorObjectRelationship',
'DiffusionCommitRelationshipSource' => 'PhabricatorObjectRelationshipSource',
'DiffusionCommitRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'DiffusionCommitRemarkupRuleTestCase' => 'PhabricatorTestCase',
'DiffusionCommitRepositoryHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitRepositoryProjectsHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitRequiredActionResultBucket' => 'DiffusionCommitResultBucket',
'DiffusionCommitResignTransaction' => 'DiffusionCommitAuditTransaction',
'DiffusionCommitResultBucket' => 'PhabricatorSearchResultBucket',
'DiffusionCommitRevertedByCommitEdgeType' => 'PhabricatorEdgeType',
'DiffusionCommitRevertsCommitEdgeType' => 'PhabricatorEdgeType',
'DiffusionCommitReviewerHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitRevisionAcceptedHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitRevisionAcceptingReviewersHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitRevisionHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitRevisionReviewersHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitRevisionSubscribersHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DiffusionCommitStateTransaction' => 'DiffusionCommitTransactionType',
'DiffusionCommitTagsController' => 'DiffusionController',
'DiffusionCommitTimelineEngine' => 'PhabricatorTimelineEngine',
'DiffusionCommitTransactionType' => 'PhabricatorModularTransactionType',
'DiffusionCommitVerifyTransaction' => 'DiffusionCommitAuditTransaction',
'DiffusionCompareController' => 'DiffusionController',
'DiffusionConduitAPIMethod' => 'ConduitAPIMethod',
'DiffusionController' => 'PhabricatorController',
'DiffusionCreateRepositoriesCapability' => 'PhabricatorPolicyCapability',
'DiffusionDaemonLockException' => 'Exception',
'DiffusionDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension',
'DiffusionDefaultEditCapability' => 'PhabricatorPolicyCapability',
'DiffusionDefaultPushCapability' => 'PhabricatorPolicyCapability',
'DiffusionDefaultViewCapability' => 'PhabricatorPolicyCapability',
'DiffusionDiffController' => 'DiffusionController',
'DiffusionDiffInlineCommentQuery' => 'PhabricatorDiffInlineCommentQuery',
'DiffusionDiffQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionDocumentController' => 'DiffusionController',
'DiffusionDocumentRenderingEngine' => 'PhabricatorDocumentRenderingEngine',
'DiffusionDoorkeeperCommitFeedStoryPublisher' => 'DoorkeeperFeedStoryPublisher',
'DiffusionEmptyResultView' => 'DiffusionView',
'DiffusionExistsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionExternalController' => 'DiffusionController',
'DiffusionExternalSymbolQuery' => 'Phobject',
'DiffusionExternalSymbolsSource' => 'Phobject',
'DiffusionFileContentQuery' => 'DiffusionFileFutureQuery',
'DiffusionFileContentQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionFileFutureQuery' => 'DiffusionQuery',
'DiffusionFindSymbolsConduitAPIMethod' => 'DiffusionConduitAPIMethod',
'DiffusionGetLintMessagesConduitAPIMethod' => 'DiffusionConduitAPIMethod',
'DiffusionGetRecentCommitsByPathConduitAPIMethod' => 'DiffusionConduitAPIMethod',
'DiffusionGitBlameQuery' => 'DiffusionBlameQuery',
'DiffusionGitBranch' => 'Phobject',
'DiffusionGitBranchTestCase' => 'PhabricatorTestCase',
'DiffusionGitCommandEngine' => 'DiffusionCommandEngine',
'DiffusionGitFileContentQuery' => 'DiffusionFileContentQuery',
'DiffusionGitLFSAuthenticateWorkflow' => 'DiffusionGitSSHWorkflow',
'DiffusionGitLFSResponse' => 'AphrontResponse',
'DiffusionGitLFSTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
'DiffusionGitRawDiffQuery' => 'DiffusionRawDiffQuery',
'DiffusionGitReceivePackSSHWorkflow' => 'DiffusionGitSSHWorkflow',
'DiffusionGitRequest' => 'DiffusionRequest',
'DiffusionGitResponse' => 'AphrontResponse',
'DiffusionGitSSHWorkflow' => array(
'DiffusionSSHWorkflow',
'DiffusionRepositoryClusterEngineLogInterface',
),
'DiffusionGitUploadPackSSHWorkflow' => 'DiffusionGitSSHWorkflow',
'DiffusionGraphController' => 'DiffusionController',
'DiffusionHistoryController' => 'DiffusionController',
'DiffusionHistoryListView' => 'DiffusionHistoryView',
'DiffusionHistoryQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionHistoryTableView' => 'DiffusionHistoryView',
'DiffusionHistoryView' => 'DiffusionView',
'DiffusionHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension',
'DiffusionIdentityAssigneeDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DiffusionIdentityAssigneeEditField' => 'PhabricatorTokenizerEditField',
'DiffusionIdentityAssigneeSearchField' => 'PhabricatorSearchTokenizerField',
'DiffusionIdentityEditController' => 'DiffusionController',
'DiffusionIdentityListController' => 'DiffusionController',
'DiffusionIdentityUnassignedDatasource' => 'PhabricatorTypeaheadDatasource',
'DiffusionIdentityViewController' => 'DiffusionController',
'DiffusionInlineCommentController' => 'PhabricatorInlineCommentController',
'DiffusionInlineCommentPreviewController' => 'PhabricatorInlineCommentPreviewController',
'DiffusionInternalAncestorsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionInternalGitRawDiffQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionLastModifiedController' => 'DiffusionController',
'DiffusionLastModifiedQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionLintController' => 'DiffusionController',
'DiffusionLintCountQuery' => 'PhabricatorQuery',
'DiffusionLintSaveRunner' => 'Phobject',
'DiffusionLocalRepositoryFilter' => 'Phobject',
'DiffusionLogController' => 'DiffusionController',
'DiffusionLookSoonConduitAPIMethod' => 'DiffusionConduitAPIMethod',
'DiffusionLowLevelCommitFieldsQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelCommitQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelFilesizeQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelGitRefQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelMercurialBranchesQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelMercurialPathsQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelParentsQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelQuery' => 'Phobject',
'DiffusionLowLevelResolveRefsQuery' => 'DiffusionLowLevelQuery',
'DiffusionMercurialBlameQuery' => 'DiffusionBlameQuery',
'DiffusionMercurialCommandEngine' => 'DiffusionCommandEngine',
'DiffusionMercurialFileContentQuery' => 'DiffusionFileContentQuery',
'DiffusionMercurialFlagInjectionException' => 'Exception',
'DiffusionMercurialRawDiffQuery' => 'DiffusionRawDiffQuery',
'DiffusionMercurialRequest' => 'DiffusionRequest',
'DiffusionMercurialResponse' => 'AphrontResponse',
'DiffusionMercurialSSHWorkflow' => 'DiffusionSSHWorkflow',
'DiffusionMercurialServeSSHWorkflow' => 'DiffusionMercurialSSHWorkflow',
'DiffusionMercurialWireClientSSHProtocolChannel' => 'PhutilProtocolChannel',
'DiffusionMercurialWireProtocol' => 'Phobject',
'DiffusionMercurialWireProtocolTests' => 'PhabricatorTestCase',
'DiffusionMercurialWireSSHTestCase' => 'PhabricatorTestCase',
'DiffusionMergedCommitsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionPathChange' => 'Phobject',
'DiffusionPathChangeQuery' => 'Phobject',
'DiffusionPathCompleteController' => 'DiffusionController',
'DiffusionPathIDQuery' => 'Phobject',
'DiffusionPathQuery' => 'Phobject',
'DiffusionPathQueryTestCase' => 'PhabricatorTestCase',
'DiffusionPathTreeController' => 'DiffusionController',
'DiffusionPathValidateController' => 'DiffusionController',
'DiffusionPatternSearchView' => 'DiffusionView',
'DiffusionPhpExternalSymbolsSource' => 'DiffusionExternalSymbolsSource',
'DiffusionPreCommitContentAffectedFilesHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentAuthorHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentAuthorProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentAuthorRawHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentBranchesHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentCommitterHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentCommitterProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentCommitterRawHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentDiffContentAddedHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentDiffContentHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentDiffContentRemovedHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentDiffEnormousHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentHeraldField' => 'HeraldField',
'DiffusionPreCommitContentMergeHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentMessageHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentPackageHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentPackageOwnerHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentPusherHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentPusherIsCommitterHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentPusherProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentRepositoryHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentRepositoryProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentRevisionAcceptedHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentRevisionAcceptingReviewersHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentRevisionHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentRevisionReviewersHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentRevisionSubscribersHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitRefChangeHeraldField' => 'DiffusionPreCommitRefHeraldField',
'DiffusionPreCommitRefHeraldField' => 'HeraldField',
'DiffusionPreCommitRefHeraldFieldGroup' => 'HeraldFieldGroup',
'DiffusionPreCommitRefNameHeraldField' => 'DiffusionPreCommitRefHeraldField',
'DiffusionPreCommitRefPusherHeraldField' => 'DiffusionPreCommitRefHeraldField',
'DiffusionPreCommitRefPusherProjectsHeraldField' => 'DiffusionPreCommitRefHeraldField',
'DiffusionPreCommitRefRepositoryHeraldField' => 'DiffusionPreCommitRefHeraldField',
'DiffusionPreCommitRefRepositoryProjectsHeraldField' => 'DiffusionPreCommitRefHeraldField',
'DiffusionPreCommitRefTypeHeraldField' => 'DiffusionPreCommitRefHeraldField',
'DiffusionPreCommitUsesGitLFSHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPullEventGarbageCollector' => 'PhabricatorGarbageCollector',
'DiffusionPullLogListController' => 'DiffusionLogController',
'DiffusionPullLogListView' => 'AphrontView',
'DiffusionPullLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DiffusionPushCapability' => 'PhabricatorPolicyCapability',
'DiffusionPushEventViewController' => 'DiffusionLogController',
'DiffusionPushLogListController' => 'DiffusionLogController',
'DiffusionPushLogListView' => 'AphrontView',
'DiffusionPythonExternalSymbolsSource' => 'DiffusionExternalSymbolsSource',
'DiffusionQuery' => 'PhabricatorQuery',
'DiffusionQueryCommitsConduitAPIMethod' => 'DiffusionConduitAPIMethod',
'DiffusionQueryConduitAPIMethod' => 'DiffusionConduitAPIMethod',
'DiffusionQueryPathsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionRawDiffQuery' => 'DiffusionFileFutureQuery',
'DiffusionRawDiffQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionReadmeView' => 'DiffusionView',
'DiffusionRefDatasource' => 'PhabricatorTypeaheadDatasource',
'DiffusionRefNotFoundException' => 'Exception',
'DiffusionRefTableController' => 'DiffusionController',
'DiffusionRefsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionRenameHistoryQuery' => 'Phobject',
'DiffusionRepositoryActionsManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryAutomationManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryBasicsManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryBranchesManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryByIDRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'DiffusionRepositoryClusterEngine' => 'Phobject',
'DiffusionRepositoryController' => 'DiffusionController',
'DiffusionRepositoryDatasource' => 'PhabricatorTypeaheadDatasource',
'DiffusionRepositoryDefaultController' => 'DiffusionController',
'DiffusionRepositoryEditActivateController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'DiffusionRepositoryEditController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryEditDangerousController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryEditDeleteController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryEditEngine' => 'PhabricatorEditEngine',
'DiffusionRepositoryEditEnormousController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryEditUpdateController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DiffusionRepositoryHistoryManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryIdentityEditor' => 'PhabricatorApplicationTransactionEditor',
'DiffusionRepositoryIdentitySearchEngine' => 'PhabricatorApplicationSearchEngine',
'DiffusionRepositoryLimitsManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryListController' => 'DiffusionController',
'DiffusionRepositoryManageController' => 'DiffusionController',
'DiffusionRepositoryManagePanelsController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryManagementBuildsPanelGroup' => 'DiffusionRepositoryManagementPanelGroup',
'DiffusionRepositoryManagementIntegrationsPanelGroup' => 'DiffusionRepositoryManagementPanelGroup',
'DiffusionRepositoryManagementMainPanelGroup' => 'DiffusionRepositoryManagementPanelGroup',
'DiffusionRepositoryManagementOtherPanelGroup' => 'DiffusionRepositoryManagementPanelGroup',
'DiffusionRepositoryManagementPanel' => 'Phobject',
'DiffusionRepositoryManagementPanelGroup' => 'Phobject',
'DiffusionRepositoryPath' => 'Phobject',
'DiffusionRepositoryPoliciesManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryProfilePictureController' => 'DiffusionController',
'DiffusionRepositoryRef' => 'Phobject',
'DiffusionRepositoryRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'DiffusionRepositorySearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DiffusionRepositoryStagingManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryStorageManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositorySubversionManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositorySymbolsManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryTag' => 'Phobject',
'DiffusionRepositoryTestAutomationController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryURICredentialController' => 'DiffusionController',
'DiffusionRepositoryURIDisableController' => 'DiffusionController',
'DiffusionRepositoryURIEditController' => 'DiffusionController',
'DiffusionRepositoryURIViewController' => 'DiffusionController',
'DiffusionRepositoryURIsIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
'DiffusionRepositoryURIsManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryURIsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'DiffusionRequest' => 'Phobject',
'DiffusionResolveRefsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionResolveUserQuery' => 'Phobject',
'DiffusionSSHWorkflow' => 'PhabricatorSSHWorkflow',
'DiffusionSearchQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionServeController' => 'DiffusionController',
'DiffusionSetPasswordSettingsPanel' => 'PhabricatorSettingsPanel',
'DiffusionSetupException' => 'Exception',
'DiffusionSubversionCommandEngine' => 'DiffusionCommandEngine',
'DiffusionSubversionSSHWorkflow' => 'DiffusionSSHWorkflow',
'DiffusionSubversionServeSSHWorkflow' => 'DiffusionSubversionSSHWorkflow',
'DiffusionSubversionWireProtocol' => 'Phobject',
'DiffusionSubversionWireProtocolTestCase' => 'PhabricatorTestCase',
'DiffusionSvnBlameQuery' => 'DiffusionBlameQuery',
'DiffusionSvnFileContentQuery' => 'DiffusionFileContentQuery',
'DiffusionSvnRawDiffQuery' => 'DiffusionRawDiffQuery',
'DiffusionSvnRequest' => 'DiffusionRequest',
'DiffusionSymbolController' => 'DiffusionController',
'DiffusionSymbolDatasource' => 'PhabricatorTypeaheadDatasource',
'DiffusionSymbolQuery' => 'PhabricatorOffsetPagedQuery',
'DiffusionSyncLogListController' => 'DiffusionLogController',
'DiffusionSyncLogListView' => 'AphrontView',
'DiffusionSyncLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DiffusionTagListController' => 'DiffusionController',
'DiffusionTagListView' => 'DiffusionView',
'DiffusionTagTableView' => 'DiffusionView',
'DiffusionTaggedRepositoriesFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DiffusionTagsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionURIEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'DiffusionURIEditEngine' => 'PhabricatorEditEngine',
'DiffusionURIEditor' => 'PhabricatorApplicationTransactionEditor',
'DiffusionURITestCase' => 'PhutilTestCase',
'DiffusionUpdateCoverageConduitAPIMethod' => 'DiffusionConduitAPIMethod',
'DiffusionView' => 'AphrontView',
'DivinerArticleAtomizer' => 'DivinerAtomizer',
'DivinerAtom' => 'Phobject',
'DivinerAtomCache' => 'DivinerDiskCache',
'DivinerAtomController' => 'DivinerController',
'DivinerAtomListController' => 'DivinerController',
'DivinerAtomPHIDType' => 'PhabricatorPHIDType',
'DivinerAtomQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DivinerAtomRef' => 'Phobject',
'DivinerAtomSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DivinerAtomizeWorkflow' => 'DivinerWorkflow',
'DivinerAtomizer' => 'Phobject',
'DivinerBookController' => 'DivinerController',
'DivinerBookDatasource' => 'PhabricatorTypeaheadDatasource',
'DivinerBookEditController' => 'DivinerController',
'DivinerBookItemView' => 'AphrontTagView',
'DivinerBookPHIDType' => 'PhabricatorPHIDType',
'DivinerBookQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DivinerController' => 'PhabricatorController',
'DivinerDAO' => 'PhabricatorLiskDAO',
'DivinerDefaultEditCapability' => 'PhabricatorPolicyCapability',
'DivinerDefaultRenderer' => 'DivinerRenderer',
'DivinerDefaultViewCapability' => 'PhabricatorPolicyCapability',
'DivinerDiskCache' => 'Phobject',
'DivinerFileAtomizer' => 'DivinerAtomizer',
'DivinerFindController' => 'DivinerController',
'DivinerGenerateWorkflow' => 'DivinerWorkflow',
'DivinerLiveAtom' => 'DivinerDAO',
'DivinerLiveBook' => array(
'DivinerDAO',
'PhabricatorPolicyInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorFulltextInterface',
),
'DivinerLiveBookEditor' => 'PhabricatorApplicationTransactionEditor',
'DivinerLiveBookFulltextEngine' => 'PhabricatorFulltextEngine',
'DivinerLiveBookTransaction' => 'PhabricatorApplicationTransaction',
'DivinerLiveBookTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'DivinerLivePublisher' => 'DivinerPublisher',
'DivinerLiveSymbol' => array(
'DivinerDAO',
'PhabricatorPolicyInterface',
'PhabricatorMarkupInterface',
'PhabricatorDestructibleInterface',
'PhabricatorFulltextInterface',
),
'DivinerLiveSymbolFulltextEngine' => 'PhabricatorFulltextEngine',
'DivinerMainController' => 'DivinerController',
'DivinerPHPAtomizer' => 'DivinerAtomizer',
'DivinerParameterTableView' => 'AphrontTagView',
'DivinerPublishCache' => 'DivinerDiskCache',
'DivinerPublisher' => 'Phobject',
'DivinerRenderer' => 'Phobject',
'DivinerReturnTableView' => 'AphrontTagView',
'DivinerSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'DivinerSectionView' => 'AphrontTagView',
'DivinerStaticPublisher' => 'DivinerPublisher',
'DivinerSymbolRemarkupRule' => 'PhutilRemarkupRule',
'DivinerWorkflow' => 'PhabricatorManagementWorkflow',
'DoorkeeperAsanaFeedWorker' => 'DoorkeeperFeedWorker',
'DoorkeeperAsanaRemarkupRule' => 'DoorkeeperRemarkupRule',
'DoorkeeperBridge' => 'Phobject',
'DoorkeeperBridgeAsana' => 'DoorkeeperBridge',
'DoorkeeperBridgeGitHub' => 'DoorkeeperBridge',
'DoorkeeperBridgeGitHubIssue' => 'DoorkeeperBridgeGitHub',
'DoorkeeperBridgeGitHubUser' => 'DoorkeeperBridgeGitHub',
'DoorkeeperBridgeJIRA' => 'DoorkeeperBridge',
'DoorkeeperBridgeJIRATestCase' => 'PhabricatorTestCase',
'DoorkeeperBridgedObjectCurtainExtension' => 'PHUICurtainExtension',
'DoorkeeperDAO' => 'PhabricatorLiskDAO',
'DoorkeeperExternalObject' => array(
'DoorkeeperDAO',
'PhabricatorPolicyInterface',
),
'DoorkeeperExternalObjectPHIDType' => 'PhabricatorPHIDType',
'DoorkeeperExternalObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DoorkeeperFeedStoryPublisher' => 'Phobject',
'DoorkeeperFeedWorker' => 'FeedPushWorker',
'DoorkeeperImportEngine' => 'Phobject',
'DoorkeeperJIRAFeedWorker' => 'DoorkeeperFeedWorker',
'DoorkeeperJIRARemarkupRule' => 'DoorkeeperRemarkupRule',
'DoorkeeperMissingLinkException' => 'Exception',
'DoorkeeperObjectRef' => 'Phobject',
'DoorkeeperRemarkupRule' => 'PhutilRemarkupRule',
'DoorkeeperSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'DoorkeeperTagView' => 'AphrontView',
'DoorkeeperTagsController' => 'PhabricatorController',
'DrydockAcquiredBrokenResourceException' => 'Exception',
'DrydockAlmanacServiceHostBlueprintImplementation' => 'DrydockBlueprintImplementation',
'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface',
'DrydockAuthorization' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
'PhabricatorConduitResultInterface',
),
'DrydockAuthorizationAuthorizeController' => 'DrydockController',
'DrydockAuthorizationListController' => 'DrydockController',
'DrydockAuthorizationListView' => 'AphrontView',
'DrydockAuthorizationPHIDType' => 'PhabricatorPHIDType',
'DrydockAuthorizationQuery' => 'DrydockQuery',
'DrydockAuthorizationSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DrydockAuthorizationSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DrydockAuthorizationViewController' => 'DrydockController',
'DrydockBlueprint' => array(
'DrydockDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorNgramsInterface',
'PhabricatorProjectInterface',
'PhabricatorConduitResultInterface',
),
'DrydockBlueprintController' => 'DrydockController',
'DrydockBlueprintCoreCustomField' => array(
'DrydockBlueprintCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'DrydockBlueprintCustomField' => 'PhabricatorCustomField',
'DrydockBlueprintDatasource' => 'PhabricatorTypeaheadDatasource',
'DrydockBlueprintDisableController' => 'DrydockBlueprintController',
'DrydockBlueprintDisableTransaction' => 'DrydockBlueprintTransactionType',
'DrydockBlueprintEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'DrydockBlueprintEditController' => 'DrydockBlueprintController',
'DrydockBlueprintEditEngine' => 'PhabricatorEditEngine',
'DrydockBlueprintEditor' => 'PhabricatorApplicationTransactionEditor',
'DrydockBlueprintImplementation' => 'Phobject',
'DrydockBlueprintImplementationTestCase' => 'PhabricatorTestCase',
'DrydockBlueprintListController' => 'DrydockBlueprintController',
'DrydockBlueprintNameNgrams' => 'PhabricatorSearchNgrams',
'DrydockBlueprintNameTransaction' => 'DrydockBlueprintTransactionType',
'DrydockBlueprintPHIDType' => 'PhabricatorPHIDType',
'DrydockBlueprintQuery' => 'DrydockQuery',
'DrydockBlueprintSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DrydockBlueprintSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DrydockBlueprintTransaction' => 'PhabricatorModularTransaction',
'DrydockBlueprintTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'DrydockBlueprintTransactionType' => 'PhabricatorModularTransactionType',
'DrydockBlueprintTypeTransaction' => 'DrydockBlueprintTransactionType',
'DrydockBlueprintViewController' => 'DrydockBlueprintController',
'DrydockCommand' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
),
'DrydockCommandError' => 'Phobject',
'DrydockCommandInterface' => 'DrydockInterface',
'DrydockCommandQuery' => 'DrydockQuery',
'DrydockConsoleController' => 'DrydockController',
'DrydockController' => 'PhabricatorController',
'DrydockCreateBlueprintsCapability' => 'PhabricatorPolicyCapability',
'DrydockDAO' => 'PhabricatorLiskDAO',
'DrydockDefaultEditCapability' => 'PhabricatorPolicyCapability',
'DrydockDefaultViewCapability' => 'PhabricatorPolicyCapability',
'DrydockFilesystemInterface' => 'DrydockInterface',
'DrydockInterface' => 'Phobject',
'DrydockLandRepositoryOperation' => 'DrydockRepositoryOperationType',
'DrydockLease' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
'PhabricatorConduitResultInterface',
),
'DrydockLeaseAcquiredLogType' => 'DrydockLogType',
'DrydockLeaseActivatedLogType' => 'DrydockLogType',
'DrydockLeaseActivationFailureLogType' => 'DrydockLogType',
'DrydockLeaseActivationYieldLogType' => 'DrydockLogType',
'DrydockLeaseAllocationFailureLogType' => 'DrydockLogType',
'DrydockLeaseController' => 'DrydockController',
'DrydockLeaseDatasource' => 'PhabricatorTypeaheadDatasource',
'DrydockLeaseDestroyedLogType' => 'DrydockLogType',
'DrydockLeaseListController' => 'DrydockLeaseController',
'DrydockLeaseListView' => 'AphrontView',
'DrydockLeaseNoAuthorizationsLogType' => 'DrydockLogType',
'DrydockLeaseNoBlueprintsLogType' => 'DrydockLogType',
'DrydockLeasePHIDType' => 'PhabricatorPHIDType',
'DrydockLeaseQuery' => 'DrydockQuery',
'DrydockLeaseQueuedLogType' => 'DrydockLogType',
'DrydockLeaseReacquireLogType' => 'DrydockLogType',
'DrydockLeaseReclaimLogType' => 'DrydockLogType',
'DrydockLeaseReleaseController' => 'DrydockLeaseController',
'DrydockLeaseReleasedLogType' => 'DrydockLogType',
'DrydockLeaseSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DrydockLeaseSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DrydockLeaseStatus' => 'PhabricatorObjectStatus',
'DrydockLeaseUpdateWorker' => 'DrydockWorker',
'DrydockLeaseViewController' => 'DrydockLeaseController',
'DrydockLeaseWaitingForResourcesLogType' => 'DrydockLogType',
'DrydockLog' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
),
'DrydockLogController' => 'DrydockController',
'DrydockLogGarbageCollector' => 'PhabricatorGarbageCollector',
'DrydockLogListController' => 'DrydockLogController',
'DrydockLogListView' => 'AphrontView',
'DrydockLogQuery' => 'DrydockQuery',
'DrydockLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DrydockLogType' => 'Phobject',
'DrydockManagementCommandWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementLeaseWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementReclaimWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementReleaseLeaseWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementReleaseResourceWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementUpdateLeaseWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementUpdateResourceWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementWorkflow' => 'PhabricatorManagementWorkflow',
'DrydockObjectAuthorizationView' => 'AphrontView',
'DrydockOperationWorkLogType' => 'DrydockLogType',
'DrydockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DrydockRepositoryOperation' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
),
'DrydockRepositoryOperationController' => 'DrydockController',
'DrydockRepositoryOperationDismissController' => 'DrydockRepositoryOperationController',
'DrydockRepositoryOperationListController' => 'DrydockRepositoryOperationController',
'DrydockRepositoryOperationPHIDType' => 'PhabricatorPHIDType',
'DrydockRepositoryOperationQuery' => 'DrydockQuery',
'DrydockRepositoryOperationSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DrydockRepositoryOperationStatusController' => 'DrydockRepositoryOperationController',
'DrydockRepositoryOperationStatusView' => 'AphrontView',
'DrydockRepositoryOperationType' => 'Phobject',
'DrydockRepositoryOperationUpdateWorker' => 'DrydockWorker',
'DrydockRepositoryOperationViewController' => 'DrydockRepositoryOperationController',
'DrydockResource' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
),
'DrydockResourceActivationFailureLogType' => 'DrydockLogType',
'DrydockResourceActivationYieldLogType' => 'DrydockLogType',
'DrydockResourceAllocationFailureLogType' => 'DrydockLogType',
'DrydockResourceController' => 'DrydockController',
'DrydockResourceDatasource' => 'PhabricatorTypeaheadDatasource',
'DrydockResourceListController' => 'DrydockResourceController',
'DrydockResourceListView' => 'AphrontView',
'DrydockResourceLockException' => 'Exception',
'DrydockResourcePHIDType' => 'PhabricatorPHIDType',
'DrydockResourceQuery' => 'DrydockQuery',
'DrydockResourceReclaimLogType' => 'DrydockLogType',
'DrydockResourceReleaseController' => 'DrydockResourceController',
'DrydockResourceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DrydockResourceStatus' => 'PhabricatorObjectStatus',
'DrydockResourceUpdateWorker' => 'DrydockWorker',
'DrydockResourceViewController' => 'DrydockResourceController',
'DrydockSFTPFilesystemInterface' => 'DrydockFilesystemInterface',
'DrydockSSHCommandInterface' => 'DrydockCommandInterface',
'DrydockSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'DrydockSlotLock' => 'DrydockDAO',
'DrydockSlotLockException' => 'Exception',
'DrydockSlotLockFailureLogType' => 'DrydockLogType',
'DrydockTestRepositoryOperation' => 'DrydockRepositoryOperationType',
'DrydockTextLogType' => 'DrydockLogType',
'DrydockWebrootInterface' => 'DrydockInterface',
'DrydockWorker' => 'PhabricatorWorker',
'DrydockWorkingCopyBlueprintImplementation' => 'DrydockBlueprintImplementation',
'EdgeSearchConduitAPIMethod' => 'ConduitAPIMethod',
'FeedConduitAPIMethod' => 'ConduitAPIMethod',
'FeedPublishConduitAPIMethod' => 'FeedConduitAPIMethod',
'FeedPublisherHTTPWorker' => 'FeedPushWorker',
'FeedPublisherWorker' => 'FeedPushWorker',
'FeedPushWorker' => 'PhabricatorWorker',
'FeedQueryConduitAPIMethod' => 'FeedConduitAPIMethod',
'FeedStoryNotificationGarbageCollector' => 'PhabricatorGarbageCollector',
'FileAllocateConduitAPIMethod' => 'FileConduitAPIMethod',
'FileConduitAPIMethod' => 'ConduitAPIMethod',
'FileCreateMailReceiver' => 'PhabricatorApplicationMailReceiver',
'FileDeletionWorker' => 'PhabricatorWorker',
'FileDownloadConduitAPIMethod' => 'FileConduitAPIMethod',
'FileInfoConduitAPIMethod' => 'FileConduitAPIMethod',
'FileMailReceiver' => 'PhabricatorObjectMailReceiver',
'FileQueryChunksConduitAPIMethod' => 'FileConduitAPIMethod',
'FileReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'FileTypeIcon' => 'Phobject',
'FileUploadChunkConduitAPIMethod' => 'FileConduitAPIMethod',
'FileUploadConduitAPIMethod' => 'FileConduitAPIMethod',
'FileUploadHashConduitAPIMethod' => 'FileConduitAPIMethod',
'FilesDefaultViewCapability' => 'PhabricatorPolicyCapability',
'FlagConduitAPIMethod' => 'ConduitAPIMethod',
'FlagDeleteConduitAPIMethod' => 'FlagConduitAPIMethod',
'FlagEditConduitAPIMethod' => 'FlagConduitAPIMethod',
'FlagQueryConduitAPIMethod' => 'FlagConduitAPIMethod',
'FundBacker' => array(
'FundDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
),
'FundBackerCart' => 'PhortuneCartImplementation',
'FundBackerEditor' => 'PhabricatorApplicationTransactionEditor',
'FundBackerListController' => 'FundController',
'FundBackerPHIDType' => 'PhabricatorPHIDType',
'FundBackerProduct' => 'PhortuneProductImplementation',
'FundBackerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'FundBackerRefundTransaction' => 'FundBackerTransactionType',
'FundBackerSearchEngine' => 'PhabricatorApplicationSearchEngine',
'FundBackerStatusTransaction' => 'FundBackerTransactionType',
'FundBackerTransaction' => 'PhabricatorModularTransaction',
'FundBackerTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'FundBackerTransactionType' => 'PhabricatorModularTransactionType',
'FundController' => 'PhabricatorController',
'FundCreateInitiativesCapability' => 'PhabricatorPolicyCapability',
'FundDAO' => 'PhabricatorLiskDAO',
'FundDefaultViewCapability' => 'PhabricatorPolicyCapability',
'FundInitiative' => array(
'FundDAO',
'PhabricatorPolicyInterface',
'PhabricatorProjectInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorSubscribableInterface',
'PhabricatorMentionableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorDestructibleInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
),
'FundInitiativeBackController' => 'FundController',
'FundInitiativeBackerTransaction' => 'FundInitiativeTransactionType',
'FundInitiativeCloseController' => 'FundController',
'FundInitiativeDescriptionTransaction' => 'FundInitiativeTransactionType',
'FundInitiativeEditController' => 'FundController',
'FundInitiativeEditEngine' => 'PhabricatorEditEngine',
'FundInitiativeEditor' => 'PhabricatorApplicationTransactionEditor',
'FundInitiativeFerretEngine' => 'PhabricatorFerretEngine',
'FundInitiativeFulltextEngine' => 'PhabricatorFulltextEngine',
'FundInitiativeListController' => 'FundController',
'FundInitiativeMerchantTransaction' => 'FundInitiativeTransactionType',
'FundInitiativeNameTransaction' => 'FundInitiativeTransactionType',
'FundInitiativePHIDType' => 'PhabricatorPHIDType',
'FundInitiativeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'FundInitiativeRefundTransaction' => 'FundInitiativeTransactionType',
'FundInitiativeRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'FundInitiativeReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'FundInitiativeRisksTransaction' => 'FundInitiativeTransactionType',
'FundInitiativeSearchEngine' => 'PhabricatorApplicationSearchEngine',
'FundInitiativeStatusTransaction' => 'FundInitiativeTransactionType',
'FundInitiativeTransaction' => 'PhabricatorModularTransaction',
'FundInitiativeTransactionComment' => 'PhabricatorApplicationTransactionComment',
'FundInitiativeTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'FundInitiativeTransactionType' => 'PhabricatorModularTransactionType',
'FundInitiativeViewController' => 'FundController',
'FundSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'HarbormasterAbortOlderBuildsBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterArcLintBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterArcUnitBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterArtifact' => 'Phobject',
'HarbormasterAutotargetsTestCase' => 'PhabricatorTestCase',
'HarbormasterBuild' => array(
'HarbormasterDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorConduitResultInterface',
'PhabricatorDestructibleInterface',
),
'HarbormasterBuildAbortedException' => 'Exception',
'HarbormasterBuildActionController' => 'HarbormasterController',
'HarbormasterBuildArcanistAutoplan' => 'HarbormasterBuildAutoplan',
'HarbormasterBuildArtifact' => array(
'HarbormasterDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'HarbormasterBuildArtifactPHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildArtifactQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildAutoplan' => 'Phobject',
'HarbormasterBuildCommand' => 'HarbormasterDAO',
'HarbormasterBuildDependencyDatasource' => 'PhabricatorTypeaheadDatasource',
'HarbormasterBuildEngine' => 'Phobject',
'HarbormasterBuildFailureException' => 'Exception',
'HarbormasterBuildGraph' => 'AbstractDirectedGraph',
'HarbormasterBuildInitiatorDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'HarbormasterBuildLintMessage' => 'HarbormasterDAO',
'HarbormasterBuildListController' => 'HarbormasterController',
'HarbormasterBuildLog' => array(
'HarbormasterDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
),
'HarbormasterBuildLogChunk' => 'HarbormasterDAO',
'HarbormasterBuildLogChunkIterator' => 'PhutilBufferedIterator',
'HarbormasterBuildLogDownloadController' => 'HarbormasterController',
'HarbormasterBuildLogPHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildLogRenderController' => 'HarbormasterController',
'HarbormasterBuildLogSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'HarbormasterBuildLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HarbormasterBuildLogTestCase' => 'PhabricatorTestCase',
'HarbormasterBuildLogView' => 'AphrontView',
'HarbormasterBuildLogViewController' => 'HarbormasterController',
'HarbormasterBuildMessage' => array(
'HarbormasterDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'HarbormasterBuildMessageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildPHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildPlan' => array(
'HarbormasterDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorSubscribableInterface',
'PhabricatorNgramsInterface',
'PhabricatorConduitResultInterface',
'PhabricatorProjectInterface',
+ 'PhabricatorPolicyCodexInterface',
),
+ 'HarbormasterBuildPlanBehavior' => 'Phobject',
+ 'HarbormasterBuildPlanBehaviorOption' => 'Phobject',
+ 'HarbormasterBuildPlanBehaviorTransaction' => 'HarbormasterBuildPlanTransactionType',
'HarbormasterBuildPlanDatasource' => 'PhabricatorTypeaheadDatasource',
'HarbormasterBuildPlanDefaultEditCapability' => 'PhabricatorPolicyCapability',
'HarbormasterBuildPlanDefaultViewCapability' => 'PhabricatorPolicyCapability',
+ 'HarbormasterBuildPlanEditAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'HarbormasterBuildPlanEditEngine' => 'PhabricatorEditEngine',
'HarbormasterBuildPlanEditor' => 'PhabricatorApplicationTransactionEditor',
'HarbormasterBuildPlanNameNgrams' => 'PhabricatorSearchNgrams',
+ 'HarbormasterBuildPlanNameTransaction' => 'HarbormasterBuildPlanTransactionType',
'HarbormasterBuildPlanPHIDType' => 'PhabricatorPHIDType',
+ 'HarbormasterBuildPlanPolicyCodex' => 'PhabricatorPolicyCodex',
'HarbormasterBuildPlanQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildPlanSearchAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'HarbormasterBuildPlanSearchEngine' => 'PhabricatorApplicationSearchEngine',
- 'HarbormasterBuildPlanTransaction' => 'PhabricatorApplicationTransaction',
+ 'HarbormasterBuildPlanStatusTransaction' => 'HarbormasterBuildPlanTransactionType',
+ 'HarbormasterBuildPlanTransaction' => 'PhabricatorModularTransaction',
'HarbormasterBuildPlanTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+ 'HarbormasterBuildPlanTransactionType' => 'PhabricatorModularTransactionType',
'HarbormasterBuildQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildRequest' => 'Phobject',
'HarbormasterBuildSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'HarbormasterBuildSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HarbormasterBuildStatus' => 'Phobject',
'HarbormasterBuildStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'HarbormasterBuildStep' => array(
'HarbormasterDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorCustomFieldInterface',
),
'HarbormasterBuildStepCoreCustomField' => array(
'HarbormasterBuildStepCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'HarbormasterBuildStepCustomField' => 'PhabricatorCustomField',
'HarbormasterBuildStepEditor' => 'PhabricatorApplicationTransactionEditor',
'HarbormasterBuildStepGroup' => 'Phobject',
'HarbormasterBuildStepImplementation' => 'Phobject',
'HarbormasterBuildStepImplementationTestCase' => 'PhabricatorTestCase',
'HarbormasterBuildStepPHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildStepQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildStepTransaction' => 'PhabricatorApplicationTransaction',
'HarbormasterBuildStepTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'HarbormasterBuildTarget' => array(
'HarbormasterDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
),
'HarbormasterBuildTargetPHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildTargetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildTargetSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HarbormasterBuildTransaction' => 'PhabricatorApplicationTransaction',
'HarbormasterBuildTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'HarbormasterBuildTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
- 'HarbormasterBuildUnitMessage' => 'HarbormasterDAO',
+ 'HarbormasterBuildUnitMessage' => array(
+ 'HarbormasterDAO',
+ 'PhabricatorPolicyInterface',
+ ),
+ 'HarbormasterBuildUnitMessageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+ 'HarbormasterBuildView' => 'AphrontView',
'HarbormasterBuildViewController' => 'HarbormasterController',
'HarbormasterBuildWorker' => 'HarbormasterWorker',
'HarbormasterBuildable' => array(
'HarbormasterDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'HarbormasterBuildableInterface',
'PhabricatorConduitResultInterface',
'PhabricatorDestructibleInterface',
),
'HarbormasterBuildableActionController' => 'HarbormasterController',
'HarbormasterBuildableEngine' => 'Phobject',
'HarbormasterBuildableListController' => 'HarbormasterController',
'HarbormasterBuildablePHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildableQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildableSearchAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'HarbormasterBuildableSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HarbormasterBuildableStatus' => 'Phobject',
'HarbormasterBuildableTransaction' => 'PhabricatorApplicationTransaction',
'HarbormasterBuildableTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'HarbormasterBuildableTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'HarbormasterBuildableViewController' => 'HarbormasterController',
'HarbormasterBuildkiteBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterBuildkiteHookController' => 'HarbormasterController',
'HarbormasterBuiltinBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterCircleCIBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterCircleCIHookController' => 'HarbormasterController',
'HarbormasterConduitAPIMethod' => 'ConduitAPIMethod',
'HarbormasterControlBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterController' => 'PhabricatorController',
'HarbormasterCreateArtifactConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
'HarbormasterCreatePlansCapability' => 'PhabricatorPolicyCapability',
'HarbormasterDAO' => 'PhabricatorLiskDAO',
'HarbormasterDrydockBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterDrydockCommandBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterDrydockLeaseArtifact' => 'HarbormasterArtifact',
'HarbormasterExecFuture' => 'Future',
'HarbormasterExternalBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterFileArtifact' => 'HarbormasterArtifact',
'HarbormasterHTTPRequestBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterHostArtifact' => 'HarbormasterDrydockLeaseArtifact',
'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterLintMessagesController' => 'HarbormasterController',
'HarbormasterLintPropertyView' => 'AphrontView',
'HarbormasterLogWorker' => 'HarbormasterWorker',
'HarbormasterManagementArchiveLogsWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementBuildWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementPublishWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementRebuildLogWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementRestartWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementUpdateWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementWorkflow' => 'PhabricatorManagementWorkflow',
'HarbormasterManagementWriteLogWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterMessageType' => 'Phobject',
'HarbormasterObject' => 'HarbormasterDAO',
'HarbormasterOtherBuildStepGroup' => 'HarbormasterBuildStepGroup',
+ 'HarbormasterPlanBehaviorController' => 'HarbormasterPlanController',
'HarbormasterPlanController' => 'HarbormasterController',
'HarbormasterPlanDisableController' => 'HarbormasterPlanController',
'HarbormasterPlanEditController' => 'HarbormasterPlanController',
'HarbormasterPlanListController' => 'HarbormasterPlanController',
'HarbormasterPlanRunController' => 'HarbormasterPlanController',
'HarbormasterPlanViewController' => 'HarbormasterPlanController',
'HarbormasterPrototypeBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterPublishFragmentBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterQueryAutotargetsConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
'HarbormasterQueryBuildablesConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
'HarbormasterQueryBuildsConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
'HarbormasterQueryBuildsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'HarbormasterRemarkupRule' => 'PhabricatorObjectRemarkupRule',
+ 'HarbormasterRestartException' => 'Exception',
'HarbormasterRunBuildPlansHeraldAction' => 'HeraldAction',
'HarbormasterSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'HarbormasterScratchTable' => 'HarbormasterDAO',
'HarbormasterSendMessageConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
'HarbormasterSleepBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterStepAddController' => 'HarbormasterPlanController',
'HarbormasterStepDeleteController' => 'HarbormasterPlanController',
'HarbormasterStepEditController' => 'HarbormasterPlanController',
'HarbormasterStepViewController' => 'HarbormasterPlanController',
+ 'HarbormasterString' => 'HarbormasterDAO',
'HarbormasterTargetEngine' => 'Phobject',
'HarbormasterTargetSearchAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'HarbormasterTargetWorker' => 'HarbormasterWorker',
'HarbormasterTestBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterThrowExceptionBuildStep' => 'HarbormasterBuildStepImplementation',
'HarbormasterUIEventListener' => 'PhabricatorEventListener',
'HarbormasterURIArtifact' => 'HarbormasterArtifact',
'HarbormasterUnitMessageListController' => 'HarbormasterController',
'HarbormasterUnitMessageViewController' => 'HarbormasterController',
'HarbormasterUnitPropertyView' => 'AphrontView',
'HarbormasterUnitStatus' => 'Phobject',
'HarbormasterUnitSummaryView' => 'AphrontView',
'HarbormasterUploadArtifactBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterWaitForPreviousBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterWorker' => 'PhabricatorWorker',
'HarbormasterWorkingCopyArtifact' => 'HarbormasterDrydockLeaseArtifact',
'HeraldActingUserField' => 'HeraldField',
'HeraldAction' => 'Phobject',
'HeraldActionGroup' => 'HeraldGroup',
'HeraldActionRecord' => 'HeraldDAO',
'HeraldAdapter' => 'Phobject',
'HeraldAdapterDatasource' => 'PhabricatorTypeaheadDatasource',
'HeraldAlwaysField' => 'HeraldField',
'HeraldAnotherRuleField' => 'HeraldField',
'HeraldApplicationActionGroup' => 'HeraldActionGroup',
'HeraldApplyTranscript' => 'Phobject',
'HeraldBasicFieldGroup' => 'HeraldFieldGroup',
'HeraldBuildableState' => 'HeraldState',
'HeraldCallWebhookAction' => 'HeraldAction',
'HeraldCommentAction' => 'HeraldAction',
'HeraldCommitAdapter' => array(
'HeraldAdapter',
'HarbormasterBuildableAdapterInterface',
),
'HeraldCondition' => 'HeraldDAO',
'HeraldConditionTranscript' => 'Phobject',
'HeraldContentSourceField' => 'HeraldField',
'HeraldController' => 'PhabricatorController',
'HeraldCoreStateReasons' => 'HeraldStateReasons',
'HeraldCreateWebhooksCapability' => 'PhabricatorPolicyCapability',
'HeraldDAO' => 'PhabricatorLiskDAO',
'HeraldDeprecatedFieldGroup' => 'HeraldFieldGroup',
'HeraldDifferentialAdapter' => 'HeraldAdapter',
'HeraldDifferentialDiffAdapter' => 'HeraldDifferentialAdapter',
'HeraldDifferentialRevisionAdapter' => array(
'HeraldDifferentialAdapter',
'HarbormasterBuildableAdapterInterface',
),
'HeraldDisableController' => 'HeraldController',
'HeraldDoNothingAction' => 'HeraldAction',
'HeraldEditFieldGroup' => 'HeraldFieldGroup',
'HeraldEffect' => 'Phobject',
'HeraldEmptyFieldValue' => 'HeraldFieldValue',
'HeraldEngine' => 'Phobject',
'HeraldExactProjectsField' => 'HeraldField',
'HeraldField' => 'Phobject',
'HeraldFieldGroup' => 'HeraldGroup',
'HeraldFieldTestCase' => 'PhutilTestCase',
'HeraldFieldValue' => 'Phobject',
'HeraldGroup' => 'Phobject',
'HeraldInvalidActionException' => 'Exception',
'HeraldInvalidConditionException' => 'Exception',
'HeraldMailableState' => 'HeraldState',
'HeraldManageGlobalRulesCapability' => 'PhabricatorPolicyCapability',
'HeraldManagementWorkflow' => 'PhabricatorManagementWorkflow',
'HeraldManiphestTaskAdapter' => 'HeraldAdapter',
'HeraldNewController' => 'HeraldController',
'HeraldNewObjectField' => 'HeraldField',
'HeraldNotifyActionGroup' => 'HeraldActionGroup',
'HeraldObjectTranscript' => 'Phobject',
'HeraldPhameBlogAdapter' => 'HeraldAdapter',
'HeraldPhamePostAdapter' => 'HeraldAdapter',
'HeraldPholioMockAdapter' => 'HeraldAdapter',
'HeraldPonderQuestionAdapter' => 'HeraldAdapter',
'HeraldPreCommitAdapter' => 'HeraldAdapter',
'HeraldPreCommitContentAdapter' => 'HeraldPreCommitAdapter',
'HeraldPreCommitRefAdapter' => 'HeraldPreCommitAdapter',
'HeraldPreventActionGroup' => 'HeraldActionGroup',
'HeraldProjectsField' => 'HeraldField',
'HeraldRecursiveConditionsException' => 'Exception',
'HeraldRelatedFieldGroup' => 'HeraldFieldGroup',
'HeraldRemarkupFieldValue' => 'HeraldFieldValue',
'HeraldRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'HeraldRule' => array(
'HeraldDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorFlaggableInterface',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
+ 'PhabricatorIndexableInterface',
'PhabricatorSubscribableInterface',
),
+ 'HeraldRuleActionAffectsObjectEdgeType' => 'PhabricatorEdgeType',
'HeraldRuleAdapter' => 'HeraldAdapter',
'HeraldRuleAdapterField' => 'HeraldRuleField',
'HeraldRuleController' => 'HeraldController',
'HeraldRuleDatasource' => 'PhabricatorTypeaheadDatasource',
+ 'HeraldRuleDisableTransaction' => 'HeraldRuleTransactionType',
+ 'HeraldRuleEditTransaction' => 'HeraldRuleTransactionType',
'HeraldRuleEditor' => 'PhabricatorApplicationTransactionEditor',
'HeraldRuleField' => 'HeraldField',
'HeraldRuleFieldGroup' => 'HeraldFieldGroup',
+ 'HeraldRuleIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
'HeraldRuleListController' => 'HeraldController',
+ 'HeraldRuleListView' => 'AphrontView',
+ 'HeraldRuleNameTransaction' => 'HeraldRuleTransactionType',
'HeraldRulePHIDType' => 'PhabricatorPHIDType',
'HeraldRuleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HeraldRuleReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'HeraldRuleSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HeraldRuleSerializer' => 'Phobject',
'HeraldRuleTestCase' => 'PhabricatorTestCase',
- 'HeraldRuleTransaction' => 'PhabricatorApplicationTransaction',
- 'HeraldRuleTransactionComment' => 'PhabricatorApplicationTransactionComment',
+ 'HeraldRuleTransaction' => 'PhabricatorModularTransaction',
+ 'HeraldRuleTransactionType' => 'PhabricatorModularTransactionType',
'HeraldRuleTranscript' => 'Phobject',
'HeraldRuleTypeConfig' => 'Phobject',
'HeraldRuleTypeDatasource' => 'PhabricatorTypeaheadDatasource',
'HeraldRuleTypeField' => 'HeraldRuleField',
'HeraldRuleViewController' => 'HeraldController',
'HeraldSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'HeraldSelectFieldValue' => 'HeraldFieldValue',
'HeraldSpaceField' => 'HeraldField',
'HeraldState' => 'Phobject',
'HeraldStateReasons' => 'Phobject',
'HeraldSubscribersField' => 'HeraldField',
'HeraldSupportActionGroup' => 'HeraldActionGroup',
'HeraldSupportFieldGroup' => 'HeraldFieldGroup',
'HeraldTestConsoleController' => 'HeraldController',
'HeraldTestManagementWorkflow' => 'HeraldManagementWorkflow',
'HeraldTextFieldValue' => 'HeraldFieldValue',
'HeraldTokenizerFieldValue' => 'HeraldFieldValue',
'HeraldTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'HeraldTranscript' => array(
'HeraldDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'HeraldTranscriptController' => 'HeraldController',
'HeraldTranscriptDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'HeraldTranscriptGarbageCollector' => 'PhabricatorGarbageCollector',
'HeraldTranscriptListController' => 'HeraldController',
'HeraldTranscriptPHIDType' => 'PhabricatorPHIDType',
'HeraldTranscriptQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HeraldTranscriptSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HeraldTranscriptTestCase' => 'PhabricatorTestCase',
'HeraldUtilityActionGroup' => 'HeraldActionGroup',
'HeraldWebhook' => array(
'HeraldDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
'PhabricatorProjectInterface',
),
'HeraldWebhookCallManagementWorkflow' => 'HeraldWebhookManagementWorkflow',
'HeraldWebhookController' => 'HeraldController',
'HeraldWebhookDatasource' => 'PhabricatorTypeaheadDatasource',
'HeraldWebhookEditController' => 'HeraldWebhookController',
'HeraldWebhookEditEngine' => 'PhabricatorEditEngine',
'HeraldWebhookEditor' => 'PhabricatorApplicationTransactionEditor',
'HeraldWebhookKeyController' => 'HeraldWebhookController',
'HeraldWebhookListController' => 'HeraldWebhookController',
'HeraldWebhookManagementWorkflow' => 'PhabricatorManagementWorkflow',
'HeraldWebhookNameTransaction' => 'HeraldWebhookTransactionType',
'HeraldWebhookPHIDType' => 'PhabricatorPHIDType',
'HeraldWebhookQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HeraldWebhookRequest' => array(
'HeraldDAO',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
),
'HeraldWebhookRequestGarbageCollector' => 'PhabricatorGarbageCollector',
'HeraldWebhookRequestListView' => 'AphrontView',
'HeraldWebhookRequestPHIDType' => 'PhabricatorPHIDType',
'HeraldWebhookRequestQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HeraldWebhookSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HeraldWebhookStatusTransaction' => 'HeraldWebhookTransactionType',
'HeraldWebhookTestController' => 'HeraldWebhookController',
'HeraldWebhookTransaction' => 'PhabricatorModularTransaction',
'HeraldWebhookTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'HeraldWebhookTransactionType' => 'PhabricatorModularTransactionType',
'HeraldWebhookURITransaction' => 'HeraldWebhookTransactionType',
'HeraldWebhookViewController' => 'HeraldWebhookController',
'HeraldWebhookWorker' => 'PhabricatorWorker',
'Javelin' => 'Phobject',
'LegalpadController' => 'PhabricatorController',
'LegalpadCreateDocumentsCapability' => 'PhabricatorPolicyCapability',
'LegalpadDAO' => 'PhabricatorLiskDAO',
'LegalpadDefaultEditCapability' => 'PhabricatorPolicyCapability',
'LegalpadDefaultViewCapability' => 'PhabricatorPolicyCapability',
'LegalpadDocument' => array(
'LegalpadDAO',
'PhabricatorPolicyInterface',
'PhabricatorSubscribableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
),
'LegalpadDocumentBody' => array(
'LegalpadDAO',
'PhabricatorMarkupInterface',
),
'LegalpadDocumentDatasource' => 'PhabricatorTypeaheadDatasource',
'LegalpadDocumentDoneController' => 'LegalpadController',
'LegalpadDocumentEditController' => 'LegalpadController',
'LegalpadDocumentEditEngine' => 'PhabricatorEditEngine',
'LegalpadDocumentEditor' => 'PhabricatorApplicationTransactionEditor',
'LegalpadDocumentListController' => 'LegalpadController',
'LegalpadDocumentManageController' => 'LegalpadController',
'LegalpadDocumentPreambleTransaction' => 'LegalpadDocumentTransactionType',
'LegalpadDocumentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'LegalpadDocumentRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'LegalpadDocumentRequireSignatureTransaction' => 'LegalpadDocumentTransactionType',
'LegalpadDocumentSearchEngine' => 'PhabricatorApplicationSearchEngine',
'LegalpadDocumentSignController' => 'LegalpadController',
'LegalpadDocumentSignature' => array(
'LegalpadDAO',
'PhabricatorPolicyInterface',
),
'LegalpadDocumentSignatureAddController' => 'LegalpadController',
'LegalpadDocumentSignatureListController' => 'LegalpadController',
'LegalpadDocumentSignatureQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'LegalpadDocumentSignatureSearchEngine' => 'PhabricatorApplicationSearchEngine',
'LegalpadDocumentSignatureTypeTransaction' => 'LegalpadDocumentTransactionType',
'LegalpadDocumentSignatureVerificationController' => 'LegalpadController',
'LegalpadDocumentSignatureViewController' => 'LegalpadController',
'LegalpadDocumentTextTransaction' => 'LegalpadDocumentTransactionType',
'LegalpadDocumentTitleTransaction' => 'LegalpadDocumentTransactionType',
'LegalpadDocumentTransactionType' => 'PhabricatorModularTransactionType',
'LegalpadMailReceiver' => 'PhabricatorObjectMailReceiver',
'LegalpadObjectNeedsSignatureEdgeType' => 'PhabricatorEdgeType',
'LegalpadReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'LegalpadRequireSignatureHeraldAction' => 'HeraldAction',
'LegalpadSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'LegalpadSignatureNeededByObjectEdgeType' => 'PhabricatorEdgeType',
'LegalpadTransaction' => 'PhabricatorModularTransaction',
'LegalpadTransactionComment' => 'PhabricatorApplicationTransactionComment',
'LegalpadTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'LiskChunkTestCase' => 'PhabricatorTestCase',
'LiskDAO' => array(
'Phobject',
'AphrontDatabaseTableRefInterface',
),
'LiskDAOTestCase' => 'PhabricatorTestCase',
'LiskEphemeralObjectException' => 'Exception',
'LiskFixtureTestCase' => 'PhabricatorTestCase',
'LiskIsolationTestCase' => 'PhabricatorTestCase',
'LiskIsolationTestDAO' => 'LiskDAO',
'LiskIsolationTestDAOException' => 'Exception',
'LiskMigrationIterator' => 'PhutilBufferedIterator',
'LiskRawMigrationIterator' => 'PhutilBufferedIterator',
'MacroConduitAPIMethod' => 'ConduitAPIMethod',
'MacroCreateMemeConduitAPIMethod' => 'MacroConduitAPIMethod',
'MacroEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'MacroEmojiExample' => 'PhabricatorUIExample',
'MacroQueryConduitAPIMethod' => 'MacroConduitAPIMethod',
'ManiphestAssignEmailCommand' => 'ManiphestEmailCommand',
'ManiphestAssigneeDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'ManiphestBulkEditCapability' => 'PhabricatorPolicyCapability',
'ManiphestBulkEditController' => 'ManiphestController',
'ManiphestClaimEmailCommand' => 'ManiphestEmailCommand',
'ManiphestCloseEmailCommand' => 'ManiphestEmailCommand',
'ManiphestConduitAPIMethod' => 'ConduitAPIMethod',
'ManiphestConfiguredCustomField' => array(
'ManiphestCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'ManiphestConstants' => 'Phobject',
'ManiphestController' => 'PhabricatorController',
'ManiphestCreateMailReceiver' => 'PhabricatorApplicationMailReceiver',
'ManiphestCreateTaskConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestCustomField' => 'PhabricatorCustomField',
'ManiphestCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
'ManiphestCustomFieldStatusParser' => 'PhabricatorCustomFieldMonogramParser',
'ManiphestCustomFieldStatusParserTestCase' => 'PhabricatorTestCase',
'ManiphestCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
'ManiphestCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
'ManiphestDAO' => 'PhabricatorLiskDAO',
'ManiphestDefaultEditCapability' => 'PhabricatorPolicyCapability',
'ManiphestDefaultViewCapability' => 'PhabricatorPolicyCapability',
'ManiphestEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'ManiphestEditEngine' => 'PhabricatorEditEngine',
'ManiphestEmailCommand' => 'MetaMTAEmailTransactionCommand',
'ManiphestGetTaskTransactionsConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension',
'ManiphestInfoConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestMailEngineExtension' => 'PhabricatorMailEngineExtension',
'ManiphestNameIndex' => 'ManiphestDAO',
'ManiphestPointsConfigType' => 'PhabricatorJSONConfigType',
'ManiphestPrioritiesConfigType' => 'PhabricatorJSONConfigType',
'ManiphestPriorityEmailCommand' => 'ManiphestEmailCommand',
'ManiphestPrioritySearchConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestProjectNameFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension',
'ManiphestQueryConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestQueryStatusesConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'ManiphestReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'ManiphestReportController' => 'ManiphestController',
'ManiphestSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'ManiphestSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'ManiphestStatusEmailCommand' => 'ManiphestEmailCommand',
'ManiphestStatusSearchConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestStatusesConfigType' => 'PhabricatorJSONConfigType',
- 'ManiphestSubpriorityController' => 'ManiphestController',
'ManiphestSubtypesConfigType' => 'PhabricatorJSONConfigType',
'ManiphestTask' => array(
'ManiphestDAO',
'PhabricatorSubscribableInterface',
'PhabricatorMarkupInterface',
'PhabricatorPolicyInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorFlaggableInterface',
'PhabricatorMentionableInterface',
'PhrequentTrackableInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorProjectInterface',
'PhabricatorSpacesInterface',
'PhabricatorConduitResultInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'DoorkeeperBridgedObjectInterface',
'PhabricatorEditEngineSubtypeInterface',
'PhabricatorEditEngineLockableInterface',
'PhabricatorEditEngineMFAInterface',
+ 'PhabricatorPolicyCodexInterface',
+ 'PhabricatorUnlockableInterface',
),
'ManiphestTaskAssignHeraldAction' => 'HeraldAction',
'ManiphestTaskAssignOtherHeraldAction' => 'ManiphestTaskAssignHeraldAction',
'ManiphestTaskAssignSelfHeraldAction' => 'ManiphestTaskAssignHeraldAction',
'ManiphestTaskAssigneeHeraldField' => 'ManiphestTaskHeraldField',
'ManiphestTaskAttachTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskAuthorHeraldField' => 'ManiphestTaskHeraldField',
'ManiphestTaskAuthorPolicyRule' => 'PhabricatorPolicyRule',
'ManiphestTaskBulkEngine' => 'PhabricatorBulkEngine',
'ManiphestTaskCloseAsDuplicateRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'ManiphestTaskCoverImageTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskDependedOnByTaskEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskDependsOnTaskEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskDescriptionHeraldField' => 'ManiphestTaskHeraldField',
'ManiphestTaskDescriptionTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskDetailController' => 'ManiphestController',
'ManiphestTaskEdgeTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskEditController' => 'ManiphestController',
'ManiphestTaskEditEngineLock' => 'PhabricatorEditEngineLock',
'ManiphestTaskFerretEngine' => 'PhabricatorFerretEngine',
'ManiphestTaskFulltextEngine' => 'PhabricatorFulltextEngine',
'ManiphestTaskGraph' => 'PhabricatorObjectGraph',
+ 'ManiphestTaskGraphController' => 'ManiphestController',
'ManiphestTaskHasCommitEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskHasCommitRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskHasDuplicateTaskEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskHasMockEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskHasMockRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskHasParentRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskHasRevisionEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskHasRevisionRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskHasSubtaskRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskHeraldField' => 'HeraldField',
'ManiphestTaskHeraldFieldGroup' => 'HeraldFieldGroup',
'ManiphestTaskIsDuplicateOfTaskEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskListController' => 'ManiphestController',
'ManiphestTaskListHTTPParameterType' => 'AphrontListHTTPParameterType',
'ManiphestTaskListView' => 'ManiphestView',
'ManiphestTaskMFAEngine' => 'PhabricatorEditEngineMFAEngine',
'ManiphestTaskMailReceiver' => 'PhabricatorObjectMailReceiver',
'ManiphestTaskMergeInRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskMergedFromTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskMergedIntoTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskOpenStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'ManiphestTaskOwnerTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskPHIDResolver' => 'PhabricatorPHIDResolver',
'ManiphestTaskPHIDType' => 'PhabricatorPHIDType',
'ManiphestTaskParentTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskPoints' => 'Phobject',
'ManiphestTaskPointsTransaction' => 'ManiphestTaskTransactionType',
+ 'ManiphestTaskPolicyCodex' => 'PhabricatorPolicyCodex',
'ManiphestTaskPriority' => 'ManiphestConstants',
'ManiphestTaskPriorityDatasource' => 'PhabricatorTypeaheadDatasource',
'ManiphestTaskPriorityHeraldAction' => 'HeraldAction',
'ManiphestTaskPriorityHeraldField' => 'ManiphestTaskHeraldField',
'ManiphestTaskPriorityTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'ManiphestTaskRelationship' => 'PhabricatorObjectRelationship',
'ManiphestTaskRelationshipSource' => 'PhabricatorObjectRelationshipSource',
'ManiphestTaskResultListView' => 'ManiphestView',
'ManiphestTaskSearchEngine' => 'PhabricatorApplicationSearchEngine',
'ManiphestTaskStatus' => 'ManiphestConstants',
'ManiphestTaskStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'ManiphestTaskStatusFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'ManiphestTaskStatusHeraldAction' => 'HeraldAction',
'ManiphestTaskStatusHeraldField' => 'ManiphestTaskHeraldField',
'ManiphestTaskStatusTestCase' => 'PhabricatorTestCase',
'ManiphestTaskStatusTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskSubpriorityTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskSubtaskController' => 'ManiphestController',
'ManiphestTaskSubtypeDatasource' => 'PhabricatorTypeaheadDatasource',
- 'ManiphestTaskTestCase' => 'PhabricatorTestCase',
'ManiphestTaskTitleHeraldField' => 'ManiphestTaskHeraldField',
'ManiphestTaskTitleTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskTransactionType' => 'PhabricatorModularTransactionType',
'ManiphestTaskUnblockTransaction' => 'ManiphestTaskTransactionType',
+ 'ManiphestTaskUnlockEngine' => 'PhabricatorUnlockEngine',
'ManiphestTransaction' => 'PhabricatorModularTransaction',
'ManiphestTransactionComment' => 'PhabricatorApplicationTransactionComment',
'ManiphestTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'ManiphestTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'ManiphestUpdateConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestView' => 'AphrontView',
'MetaMTAEmailTransactionCommand' => 'Phobject',
'MetaMTAEmailTransactionCommandTestCase' => 'PhabricatorTestCase',
'MetaMTAMailReceivedGarbageCollector' => 'PhabricatorGarbageCollector',
'MetaMTAMailSentGarbageCollector' => 'PhabricatorGarbageCollector',
'MetaMTAReceivedMailStatus' => 'Phobject',
'MultimeterContext' => 'MultimeterDimension',
'MultimeterControl' => 'Phobject',
'MultimeterController' => 'PhabricatorController',
'MultimeterDAO' => 'PhabricatorLiskDAO',
'MultimeterDimension' => 'MultimeterDAO',
'MultimeterEvent' => 'MultimeterDAO',
'MultimeterEventGarbageCollector' => 'PhabricatorGarbageCollector',
'MultimeterHost' => 'MultimeterDimension',
'MultimeterLabel' => 'MultimeterDimension',
'MultimeterSampleController' => 'MultimeterController',
'MultimeterViewer' => 'MultimeterDimension',
'NuanceCommandImplementation' => 'Phobject',
'NuanceConduitAPIMethod' => 'ConduitAPIMethod',
'NuanceConsoleController' => 'NuanceController',
'NuanceContentSource' => 'PhabricatorContentSource',
'NuanceController' => 'PhabricatorController',
'NuanceDAO' => 'PhabricatorLiskDAO',
'NuanceFormItemType' => 'NuanceItemType',
'NuanceGitHubEventItemType' => 'NuanceItemType',
'NuanceGitHubImportCursor' => 'NuanceImportCursor',
'NuanceGitHubIssuesImportCursor' => 'NuanceGitHubImportCursor',
'NuanceGitHubRawEvent' => 'Phobject',
'NuanceGitHubRawEventTestCase' => 'PhabricatorTestCase',
'NuanceGitHubRepositoryImportCursor' => 'NuanceGitHubImportCursor',
'NuanceGitHubRepositorySourceDefinition' => 'NuanceSourceDefinition',
'NuanceImportCursor' => 'Phobject',
'NuanceImportCursorData' => array(
'NuanceDAO',
'PhabricatorPolicyInterface',
),
'NuanceImportCursorDataQuery' => 'NuanceQuery',
'NuanceImportCursorPHIDType' => 'PhabricatorPHIDType',
'NuanceItem' => array(
'NuanceDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
),
'NuanceItemActionController' => 'NuanceController',
'NuanceItemCommand' => array(
'NuanceDAO',
'PhabricatorPolicyInterface',
),
'NuanceItemCommandQuery' => 'NuanceQuery',
'NuanceItemCommandSpec' => 'Phobject',
'NuanceItemCommandTransaction' => 'NuanceItemTransactionType',
'NuanceItemController' => 'NuanceController',
'NuanceItemEditor' => 'PhabricatorApplicationTransactionEditor',
'NuanceItemListController' => 'NuanceItemController',
'NuanceItemManageController' => 'NuanceController',
'NuanceItemOwnerTransaction' => 'NuanceItemTransactionType',
'NuanceItemPHIDType' => 'PhabricatorPHIDType',
'NuanceItemPropertyTransaction' => 'NuanceItemTransactionType',
'NuanceItemQuery' => 'NuanceQuery',
'NuanceItemQueueTransaction' => 'NuanceItemTransactionType',
'NuanceItemRequestorTransaction' => 'NuanceItemTransactionType',
'NuanceItemSearchEngine' => 'PhabricatorApplicationSearchEngine',
'NuanceItemSourceTransaction' => 'NuanceItemTransactionType',
'NuanceItemStatusTransaction' => 'NuanceItemTransactionType',
'NuanceItemTransaction' => 'NuanceTransaction',
'NuanceItemTransactionComment' => 'PhabricatorApplicationTransactionComment',
'NuanceItemTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'NuanceItemTransactionType' => 'PhabricatorModularTransactionType',
'NuanceItemType' => 'Phobject',
'NuanceItemUpdateWorker' => 'NuanceWorker',
'NuanceItemViewController' => 'NuanceController',
'NuanceManagementImportWorkflow' => 'NuanceManagementWorkflow',
'NuanceManagementUpdateWorkflow' => 'NuanceManagementWorkflow',
'NuanceManagementWorkflow' => 'PhabricatorManagementWorkflow',
'NuancePhabricatorFormSourceDefinition' => 'NuanceSourceDefinition',
'NuanceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'NuanceQueue' => array(
'NuanceDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
),
'NuanceQueueController' => 'NuanceController',
'NuanceQueueDatasource' => 'PhabricatorTypeaheadDatasource',
'NuanceQueueEditController' => 'NuanceQueueController',
'NuanceQueueEditEngine' => 'PhabricatorEditEngine',
'NuanceQueueEditor' => 'PhabricatorApplicationTransactionEditor',
'NuanceQueueListController' => 'NuanceQueueController',
'NuanceQueueNameTransaction' => 'NuanceQueueTransactionType',
'NuanceQueuePHIDType' => 'PhabricatorPHIDType',
'NuanceQueueQuery' => 'NuanceQuery',
'NuanceQueueSearchEngine' => 'PhabricatorApplicationSearchEngine',
'NuanceQueueTransaction' => 'NuanceTransaction',
'NuanceQueueTransactionComment' => 'PhabricatorApplicationTransactionComment',
'NuanceQueueTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'NuanceQueueTransactionType' => 'PhabricatorModularTransactionType',
'NuanceQueueViewController' => 'NuanceQueueController',
'NuanceQueueWorkController' => 'NuanceQueueController',
'NuanceSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'NuanceSource' => array(
'NuanceDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorNgramsInterface',
),
'NuanceSourceActionController' => 'NuanceController',
'NuanceSourceController' => 'NuanceController',
'NuanceSourceDefaultEditCapability' => 'PhabricatorPolicyCapability',
'NuanceSourceDefaultQueueTransaction' => 'NuanceSourceTransactionType',
'NuanceSourceDefaultViewCapability' => 'PhabricatorPolicyCapability',
'NuanceSourceDefinition' => 'Phobject',
'NuanceSourceDefinitionTestCase' => 'PhabricatorTestCase',
'NuanceSourceEditController' => 'NuanceSourceController',
'NuanceSourceEditEngine' => 'PhabricatorEditEngine',
'NuanceSourceEditor' => 'PhabricatorApplicationTransactionEditor',
'NuanceSourceListController' => 'NuanceSourceController',
'NuanceSourceManageCapability' => 'PhabricatorPolicyCapability',
'NuanceSourceNameNgrams' => 'PhabricatorSearchNgrams',
'NuanceSourceNameTransaction' => 'NuanceSourceTransactionType',
'NuanceSourcePHIDType' => 'PhabricatorPHIDType',
'NuanceSourceQuery' => 'NuanceQuery',
'NuanceSourceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'NuanceSourceTransaction' => 'NuanceTransaction',
'NuanceSourceTransactionComment' => 'PhabricatorApplicationTransactionComment',
'NuanceSourceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'NuanceSourceTransactionType' => 'PhabricatorModularTransactionType',
'NuanceSourceViewController' => 'NuanceSourceController',
'NuanceTransaction' => 'PhabricatorModularTransaction',
'NuanceTrashCommand' => 'NuanceCommandImplementation',
'NuanceWorker' => 'PhabricatorWorker',
'OwnersConduitAPIMethod' => 'ConduitAPIMethod',
'OwnersEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'OwnersPackageReplyHandler' => 'PhabricatorMailReplyHandler',
'OwnersQueryConduitAPIMethod' => 'OwnersConduitAPIMethod',
'OwnersSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PHIDConduitAPIMethod' => 'ConduitAPIMethod',
'PHIDInfoConduitAPIMethod' => 'PHIDConduitAPIMethod',
'PHIDLookupConduitAPIMethod' => 'PHIDConduitAPIMethod',
'PHIDQueryConduitAPIMethod' => 'PHIDConduitAPIMethod',
'PHUI' => 'Phobject',
'PHUIActionPanelExample' => 'PhabricatorUIExample',
'PHUIActionPanelView' => 'AphrontTagView',
'PHUIApplicationMenuView' => 'Phobject',
'PHUIBadgeBoxView' => 'AphrontTagView',
'PHUIBadgeExample' => 'PhabricatorUIExample',
'PHUIBadgeMiniView' => 'AphrontTagView',
'PHUIBadgeView' => 'AphrontTagView',
'PHUIBigInfoExample' => 'PhabricatorUIExample',
'PHUIBigInfoView' => 'AphrontTagView',
'PHUIBoxExample' => 'PhabricatorUIExample',
'PHUIBoxView' => 'AphrontTagView',
'PHUIButtonBarExample' => 'PhabricatorUIExample',
'PHUIButtonBarView' => 'AphrontTagView',
'PHUIButtonExample' => 'PhabricatorUIExample',
'PHUIButtonView' => 'AphrontTagView',
'PHUICMSView' => 'AphrontTagView',
'PHUICalendarDayView' => 'AphrontView',
'PHUICalendarListView' => 'AphrontTagView',
'PHUICalendarMonthView' => 'AphrontView',
'PHUICalendarWeekView' => 'AphrontView',
'PHUICalendarWidgetView' => 'AphrontTagView',
'PHUIColorPalletteExample' => 'PhabricatorUIExample',
'PHUICrumbView' => 'AphrontView',
'PHUICrumbsView' => 'AphrontView',
'PHUICurtainExtension' => 'Phobject',
'PHUICurtainPanelView' => 'AphrontTagView',
'PHUICurtainView' => 'AphrontTagView',
'PHUIDiffGraphView' => 'Phobject',
'PHUIDiffGraphViewTestCase' => 'PhabricatorTestCase',
'PHUIDiffInlineCommentDetailView' => 'PHUIDiffInlineCommentView',
'PHUIDiffInlineCommentEditView' => 'PHUIDiffInlineCommentView',
'PHUIDiffInlineCommentPreviewListView' => 'AphrontView',
'PHUIDiffInlineCommentRowScaffold' => 'AphrontView',
'PHUIDiffInlineCommentTableScaffold' => 'AphrontView',
'PHUIDiffInlineCommentUndoView' => 'PHUIDiffInlineCommentView',
'PHUIDiffInlineCommentView' => 'AphrontView',
'PHUIDiffInlineThreader' => 'Phobject',
'PHUIDiffOneUpInlineCommentRowScaffold' => 'PHUIDiffInlineCommentRowScaffold',
'PHUIDiffRevealIconView' => 'AphrontView',
'PHUIDiffTableOfContentsItemView' => 'AphrontView',
'PHUIDiffTableOfContentsListView' => 'AphrontView',
'PHUIDiffTwoUpInlineCommentRowScaffold' => 'PHUIDiffInlineCommentRowScaffold',
'PHUIDocumentSummaryView' => 'AphrontTagView',
'PHUIDocumentView' => 'AphrontTagView',
'PHUIFeedStoryExample' => 'PhabricatorUIExample',
'PHUIFeedStoryView' => 'AphrontView',
'PHUIFormDividerControl' => 'AphrontFormControl',
'PHUIFormFileControl' => 'AphrontFormControl',
'PHUIFormFreeformDateControl' => 'AphrontFormControl',
'PHUIFormIconSetControl' => 'AphrontFormControl',
'PHUIFormInsetView' => 'AphrontView',
'PHUIFormLayoutView' => 'AphrontView',
'PHUIFormNumberControl' => 'AphrontFormControl',
'PHUIFormTimerControl' => 'AphrontFormControl',
'PHUIHandleListView' => 'AphrontTagView',
'PHUIHandleTagListView' => 'AphrontTagView',
'PHUIHandleView' => 'AphrontView',
'PHUIHeadThingView' => 'AphrontTagView',
'PHUIHeaderView' => 'AphrontTagView',
'PHUIHomeView' => 'AphrontTagView',
'PHUIHovercardUIExample' => 'PhabricatorUIExample',
'PHUIHovercardView' => 'AphrontTagView',
'PHUIIconCircleView' => 'AphrontTagView',
'PHUIIconExample' => 'PhabricatorUIExample',
'PHUIIconView' => 'AphrontTagView',
'PHUIImageMaskExample' => 'PhabricatorUIExample',
'PHUIImageMaskView' => 'AphrontTagView',
'PHUIInfoExample' => 'PhabricatorUIExample',
'PHUIInfoView' => 'AphrontTagView',
'PHUIInvisibleCharacterTestCase' => 'PhabricatorTestCase',
'PHUIInvisibleCharacterView' => 'AphrontView',
'PHUILeftRightExample' => 'PhabricatorUIExample',
'PHUILeftRightView' => 'AphrontTagView',
'PHUIListExample' => 'PhabricatorUIExample',
'PHUIListItemView' => 'AphrontTagView',
'PHUIListView' => 'AphrontTagView',
'PHUIListViewTestCase' => 'PhabricatorTestCase',
'PHUIObjectBoxView' => 'AphrontTagView',
'PHUIObjectItemListExample' => 'PhabricatorUIExample',
'PHUIObjectItemListView' => 'AphrontTagView',
'PHUIObjectItemView' => 'AphrontTagView',
'PHUIPagerView' => 'AphrontView',
'PHUIPinboardItemView' => 'AphrontView',
'PHUIPinboardView' => 'AphrontView',
'PHUIPolicySectionView' => 'AphrontTagView',
'PHUIPropertyGroupView' => 'AphrontTagView',
'PHUIPropertyListExample' => 'PhabricatorUIExample',
'PHUIPropertyListView' => 'AphrontView',
'PHUIRemarkupImageView' => 'AphrontView',
'PHUIRemarkupPreviewPanel' => 'AphrontTagView',
'PHUIRemarkupView' => 'AphrontView',
'PHUISegmentBarSegmentView' => 'AphrontTagView',
'PHUISegmentBarView' => 'AphrontTagView',
'PHUISpacesNamespaceContextView' => 'AphrontView',
'PHUIStatusItemView' => 'AphrontTagView',
'PHUIStatusListView' => 'AphrontTagView',
'PHUITabGroupView' => 'AphrontTagView',
'PHUITabView' => 'AphrontTagView',
'PHUITagExample' => 'PhabricatorUIExample',
'PHUITagView' => 'AphrontTagView',
'PHUITimelineEventView' => 'AphrontView',
'PHUITimelineExample' => 'PhabricatorUIExample',
'PHUITimelineView' => 'AphrontView',
'PHUITwoColumnView' => 'AphrontTagView',
'PHUITypeaheadExample' => 'PhabricatorUIExample',
'PHUIUserAvailabilityView' => 'AphrontTagView',
'PHUIWorkboardView' => 'AphrontTagView',
'PHUIWorkpanelView' => 'AphrontTagView',
'PHUIXComponentsExample' => 'PhabricatorUIExample',
'PassphraseAbstractKey' => 'Phobject',
'PassphraseConduitAPIMethod' => 'ConduitAPIMethod',
'PassphraseController' => 'PhabricatorController',
'PassphraseCredential' => array(
'PassphraseDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorFlaggableInterface',
'PhabricatorSubscribableInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSpacesInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
),
'PassphraseCredentialAuthorPolicyRule' => 'PhabricatorPolicyRule',
'PassphraseCredentialConduitController' => 'PassphraseController',
'PassphraseCredentialConduitTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialControl' => 'AphrontFormControl',
'PassphraseCredentialCreateController' => 'PassphraseController',
'PassphraseCredentialDescriptionTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialDestroyController' => 'PassphraseController',
'PassphraseCredentialDestroyTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialEditController' => 'PassphraseController',
'PassphraseCredentialFerretEngine' => 'PhabricatorFerretEngine',
'PassphraseCredentialFulltextEngine' => 'PhabricatorFulltextEngine',
'PassphraseCredentialListController' => 'PassphraseController',
'PassphraseCredentialLockController' => 'PassphraseController',
'PassphraseCredentialLockTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialLookedAtTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialNameTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialPHIDType' => 'PhabricatorPHIDType',
'PassphraseCredentialPublicController' => 'PassphraseController',
'PassphraseCredentialQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PassphraseCredentialRevealController' => 'PassphraseController',
'PassphraseCredentialSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PassphraseCredentialSecretIDTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialTransaction' => 'PhabricatorModularTransaction',
'PassphraseCredentialTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PassphraseCredentialTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PassphraseCredentialTransactionType' => 'PhabricatorModularTransactionType',
'PassphraseCredentialType' => 'Phobject',
'PassphraseCredentialTypeTestCase' => 'PhabricatorTestCase',
'PassphraseCredentialUsernameTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialViewController' => 'PassphraseController',
'PassphraseDAO' => 'PhabricatorLiskDAO',
'PassphraseDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PassphraseDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PassphraseNoteCredentialType' => 'PassphraseCredentialType',
'PassphrasePasswordCredentialType' => 'PassphraseCredentialType',
'PassphrasePasswordKey' => 'PassphraseAbstractKey',
'PassphraseQueryConduitAPIMethod' => 'PassphraseConduitAPIMethod',
'PassphraseRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PassphraseSSHGeneratedKeyCredentialType' => 'PassphraseSSHPrivateKeyCredentialType',
'PassphraseSSHKey' => 'PassphraseAbstractKey',
'PassphraseSSHPrivateKeyCredentialType' => 'PassphraseCredentialType',
'PassphraseSSHPrivateKeyFileCredentialType' => 'PassphraseSSHPrivateKeyCredentialType',
'PassphraseSSHPrivateKeyTextCredentialType' => 'PassphraseSSHPrivateKeyCredentialType',
'PassphraseSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PassphraseSecret' => 'PassphraseDAO',
'PassphraseTokenCredentialType' => 'PassphraseCredentialType',
'PasteConduitAPIMethod' => 'ConduitAPIMethod',
'PasteCreateConduitAPIMethod' => 'PasteConduitAPIMethod',
'PasteCreateMailReceiver' => 'PhabricatorApplicationMailReceiver',
'PasteDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PasteDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PasteEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PasteEmbedView' => 'AphrontView',
'PasteInfoConduitAPIMethod' => 'PasteConduitAPIMethod',
'PasteLanguageSelectDatasource' => 'PhabricatorTypeaheadDatasource',
'PasteMailReceiver' => 'PhabricatorObjectMailReceiver',
'PasteQueryConduitAPIMethod' => 'PasteConduitAPIMethod',
'PasteReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PasteSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PeopleBrowseUserDirectoryCapability' => 'PhabricatorPolicyCapability',
'PeopleCreateUsersCapability' => 'PhabricatorPolicyCapability',
'PeopleDisableUsersCapability' => 'PhabricatorPolicyCapability',
'PeopleHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension',
'PeopleMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension',
'PeopleUserLogGarbageCollector' => 'PhabricatorGarbageCollector',
'Phabricator404Controller' => 'PhabricatorController',
'PhabricatorAWSConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorAccessControlTestCase' => 'PhabricatorTestCase',
'PhabricatorAccessLog' => 'Phobject',
'PhabricatorAccessLogConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorAccessibilitySetting' => 'PhabricatorSelectSetting',
'PhabricatorActionListView' => 'AphrontTagView',
'PhabricatorActionView' => 'AphrontView',
'PhabricatorActivitySettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorAdministratorsPolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorAjaxRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorAlmanacApplication' => 'PhabricatorApplication',
'PhabricatorAmazonAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorAmazonSNSFuture' => 'PhutilAWSFuture',
'PhabricatorAnchorView' => 'AphrontView',
'PhabricatorAphlictManagementDebugWorkflow' => 'PhabricatorAphlictManagementWorkflow',
'PhabricatorAphlictManagementNotifyWorkflow' => 'PhabricatorAphlictManagementWorkflow',
'PhabricatorAphlictManagementRestartWorkflow' => 'PhabricatorAphlictManagementWorkflow',
'PhabricatorAphlictManagementStartWorkflow' => 'PhabricatorAphlictManagementWorkflow',
'PhabricatorAphlictManagementStatusWorkflow' => 'PhabricatorAphlictManagementWorkflow',
'PhabricatorAphlictManagementStopWorkflow' => 'PhabricatorAphlictManagementWorkflow',
'PhabricatorAphlictManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorAphlictSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorAphrontBarUIExample' => 'PhabricatorUIExample',
'PhabricatorAphrontViewTestCase' => 'PhabricatorTestCase',
'PhabricatorAppSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorApplication' => array(
'PhabricatorLiskDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhabricatorApplicationApplicationPHIDType' => 'PhabricatorPHIDType',
'PhabricatorApplicationApplicationTransaction' => 'PhabricatorModularTransaction',
'PhabricatorApplicationApplicationTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorApplicationConfigOptions' => 'Phobject',
'PhabricatorApplicationConfigurationPanel' => 'Phobject',
'PhabricatorApplicationConfigurationPanelTestCase' => 'PhabricatorTestCase',
'PhabricatorApplicationDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorApplicationDetailViewController' => 'PhabricatorApplicationsController',
'PhabricatorApplicationEditController' => 'PhabricatorApplicationsController',
'PhabricatorApplicationEditEngine' => 'PhabricatorEditEngine',
'PhabricatorApplicationEditHTTPParameterHelpView' => 'AphrontView',
'PhabricatorApplicationEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorApplicationEmailCommandsController' => 'PhabricatorApplicationsController',
'PhabricatorApplicationMailReceiver' => 'PhabricatorMailReceiver',
'PhabricatorApplicationObjectMailEngineExtension' => 'PhabricatorMailEngineExtension',
'PhabricatorApplicationPanelController' => 'PhabricatorApplicationsController',
'PhabricatorApplicationPolicyChangeTransaction' => 'PhabricatorApplicationTransactionType',
'PhabricatorApplicationProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorApplicationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorApplicationSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorApplicationSearchController' => 'PhabricatorSearchBaseController',
'PhabricatorApplicationSearchEngine' => 'Phobject',
'PhabricatorApplicationSearchEngineTestCase' => 'PhabricatorTestCase',
'PhabricatorApplicationSearchResultView' => 'Phobject',
'PhabricatorApplicationTestCase' => 'PhabricatorTestCase',
'PhabricatorApplicationTransaction' => array(
'PhabricatorLiskDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorApplicationTransactionComment' => array(
'PhabricatorLiskDAO',
'PhabricatorMarkupInterface',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorApplicationTransactionCommentEditController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionCommentEditor' => 'PhabricatorEditor',
'PhabricatorApplicationTransactionCommentHistoryController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionCommentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorApplicationTransactionCommentQuoteController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionCommentRawController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionCommentRemoveController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionCommentView' => 'AphrontView',
'PhabricatorApplicationTransactionController' => 'PhabricatorController',
'PhabricatorApplicationTransactionDetailController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionEditor' => 'PhabricatorEditor',
'PhabricatorApplicationTransactionFeedStory' => 'PhabricatorFeedStory',
'PhabricatorApplicationTransactionNoEffectException' => 'Exception',
'PhabricatorApplicationTransactionNoEffectResponse' => 'AphrontProxyResponse',
'PhabricatorApplicationTransactionPublishWorker' => 'PhabricatorWorker',
'PhabricatorApplicationTransactionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorApplicationTransactionRemarkupPreviewController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionReplyHandler' => 'PhabricatorMailReplyHandler',
'PhabricatorApplicationTransactionResponse' => 'AphrontProxyResponse',
'PhabricatorApplicationTransactionShowOlderController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionStructureException' => 'Exception',
'PhabricatorApplicationTransactionTemplatedCommentQuery' => 'PhabricatorApplicationTransactionCommentQuery',
'PhabricatorApplicationTransactionTextDiffDetailView' => 'AphrontView',
'PhabricatorApplicationTransactionTransactionPHIDType' => 'PhabricatorPHIDType',
'PhabricatorApplicationTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorApplicationTransactionValidationError' => 'Phobject',
'PhabricatorApplicationTransactionValidationException' => 'Exception',
'PhabricatorApplicationTransactionValidationResponse' => 'AphrontProxyResponse',
'PhabricatorApplicationTransactionValueController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionView' => 'AphrontView',
'PhabricatorApplicationTransactionWarningException' => 'Exception',
'PhabricatorApplicationTransactionWarningResponse' => 'AphrontProxyResponse',
'PhabricatorApplicationUninstallController' => 'PhabricatorApplicationsController',
'PhabricatorApplicationUninstallTransaction' => 'PhabricatorApplicationTransactionType',
'PhabricatorApplicationsApplication' => 'PhabricatorApplication',
'PhabricatorApplicationsController' => 'PhabricatorController',
'PhabricatorApplicationsListController' => 'PhabricatorApplicationsController',
'PhabricatorApplyEditField' => 'PhabricatorEditField',
'PhabricatorAsanaAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorAsanaConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorAsanaSubtaskHasObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorAsanaTaskHasObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorAudioDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorAuditActionConstants' => 'Phobject',
'PhabricatorAuditApplication' => 'PhabricatorApplication',
'PhabricatorAuditCommentEditor' => 'PhabricatorEditor',
'PhabricatorAuditController' => 'PhabricatorController',
'PhabricatorAuditEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorAuditInlineComment' => array(
'Phobject',
'PhabricatorInlineCommentInterface',
),
'PhabricatorAuditListView' => 'AphrontView',
'PhabricatorAuditMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorAuditManagementDeleteWorkflow' => 'PhabricatorAuditManagementWorkflow',
'PhabricatorAuditManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorAuditReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorAuditStatusConstants' => 'Phobject',
'PhabricatorAuditSynchronizeManagementWorkflow' => 'PhabricatorAuditManagementWorkflow',
'PhabricatorAuditTransaction' => 'PhabricatorModularTransaction',
'PhabricatorAuditTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorAuditTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorAuditTransactionView' => 'PhabricatorApplicationTransactionView',
'PhabricatorAuditUpdateOwnersManagementWorkflow' => 'PhabricatorAuditManagementWorkflow',
'PhabricatorAuthAccountView' => 'AphrontView',
'PhabricatorAuthApplication' => 'PhabricatorApplication',
'PhabricatorAuthAuthFactorPHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthAuthFactorProviderPHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthAuthProviderPHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthCSRFEngine' => 'Phobject',
'PhabricatorAuthChallenge' => array(
'PhabricatorAuthDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorAuthChallengeGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorAuthChallengePHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthChallengeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+ 'PhabricatorAuthChallengeStatusController' => 'PhabricatorAuthController',
+ 'PhabricatorAuthChallengeUpdate' => 'Phobject',
'PhabricatorAuthChangePasswordAction' => 'PhabricatorSystemAction',
'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod',
'PhabricatorAuthConduitTokenRevoker' => 'PhabricatorAuthRevoker',
'PhabricatorAuthConfirmLinkController' => 'PhabricatorAuthController',
'PhabricatorAuthContactNumber' => array(
'PhabricatorAuthDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorEditEngineMFAInterface',
),
'PhabricatorAuthContactNumberController' => 'PhabricatorAuthController',
'PhabricatorAuthContactNumberDisableController' => 'PhabricatorAuthContactNumberController',
'PhabricatorAuthContactNumberEditController' => 'PhabricatorAuthContactNumberController',
'PhabricatorAuthContactNumberEditEngine' => 'PhabricatorEditEngine',
'PhabricatorAuthContactNumberEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorAuthContactNumberMFAEngine' => 'PhabricatorEditEngineMFAEngine',
'PhabricatorAuthContactNumberNumberTransaction' => 'PhabricatorAuthContactNumberTransactionType',
'PhabricatorAuthContactNumberPHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthContactNumberPrimaryController' => 'PhabricatorAuthContactNumberController',
'PhabricatorAuthContactNumberPrimaryTransaction' => 'PhabricatorAuthContactNumberTransactionType',
'PhabricatorAuthContactNumberQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthContactNumberStatusTransaction' => 'PhabricatorAuthContactNumberTransactionType',
'PhabricatorAuthContactNumberTestController' => 'PhabricatorAuthContactNumberController',
'PhabricatorAuthContactNumberTransaction' => 'PhabricatorModularTransaction',
'PhabricatorAuthContactNumberTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorAuthContactNumberTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorAuthContactNumberViewController' => 'PhabricatorAuthContactNumberController',
'PhabricatorAuthController' => 'PhabricatorController',
'PhabricatorAuthDAO' => 'PhabricatorLiskDAO',
'PhabricatorAuthDisableController' => 'PhabricatorAuthProviderConfigController',
'PhabricatorAuthDowngradeSessionController' => 'PhabricatorAuthController',
'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController',
'PhabricatorAuthFactor' => 'Phobject',
'PhabricatorAuthFactorConfig' => array(
'PhabricatorAuthDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorAuthFactorConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthFactorProvider' => array(
'PhabricatorAuthDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorEditEngineMFAInterface',
),
'PhabricatorAuthFactorProviderController' => 'PhabricatorAuthProviderController',
'PhabricatorAuthFactorProviderDuoCredentialTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
'PhabricatorAuthFactorProviderDuoEnrollTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
'PhabricatorAuthFactorProviderDuoHostnameTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
'PhabricatorAuthFactorProviderDuoUsernamesTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
'PhabricatorAuthFactorProviderEditController' => 'PhabricatorAuthFactorProviderController',
'PhabricatorAuthFactorProviderEditEngine' => 'PhabricatorEditEngine',
'PhabricatorAuthFactorProviderEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorAuthFactorProviderEnrollMessageTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
'PhabricatorAuthFactorProviderListController' => 'PhabricatorAuthProviderController',
'PhabricatorAuthFactorProviderMFAEngine' => 'PhabricatorEditEngineMFAEngine',
'PhabricatorAuthFactorProviderMessageController' => 'PhabricatorAuthFactorProviderController',
'PhabricatorAuthFactorProviderNameTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
'PhabricatorAuthFactorProviderQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthFactorProviderStatus' => 'Phobject',
'PhabricatorAuthFactorProviderStatusTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
'PhabricatorAuthFactorProviderTransaction' => 'PhabricatorModularTransaction',
'PhabricatorAuthFactorProviderTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorAuthFactorProviderTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorAuthFactorProviderViewController' => 'PhabricatorAuthFactorProviderController',
'PhabricatorAuthFactorResult' => 'Phobject',
'PhabricatorAuthFactorTestCase' => 'PhabricatorTestCase',
'PhabricatorAuthFinishController' => 'PhabricatorAuthController',
'PhabricatorAuthHMACKey' => 'PhabricatorAuthDAO',
'PhabricatorAuthHighSecurityRequiredException' => 'Exception',
'PhabricatorAuthHighSecurityToken' => 'Phobject',
'PhabricatorAuthInvite' => array(
'PhabricatorUserDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorAuthInviteAccountException' => 'PhabricatorAuthInviteDialogException',
'PhabricatorAuthInviteAction' => 'Phobject',
'PhabricatorAuthInviteActionTableView' => 'AphrontView',
'PhabricatorAuthInviteController' => 'PhabricatorAuthController',
'PhabricatorAuthInviteDialogException' => 'PhabricatorAuthInviteException',
'PhabricatorAuthInviteEngine' => 'Phobject',
'PhabricatorAuthInviteException' => 'Exception',
'PhabricatorAuthInviteInvalidException' => 'PhabricatorAuthInviteDialogException',
'PhabricatorAuthInviteLoginException' => 'PhabricatorAuthInviteDialogException',
'PhabricatorAuthInvitePHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthInviteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthInviteRegisteredException' => 'PhabricatorAuthInviteException',
'PhabricatorAuthInviteSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorAuthInviteTestCase' => 'PhabricatorTestCase',
'PhabricatorAuthInviteVerifyException' => 'PhabricatorAuthInviteDialogException',
'PhabricatorAuthInviteWorker' => 'PhabricatorWorker',
'PhabricatorAuthLinkController' => 'PhabricatorAuthController',
+ 'PhabricatorAuthLinkMessageType' => 'PhabricatorAuthMessageType',
'PhabricatorAuthListController' => 'PhabricatorAuthProviderConfigController',
'PhabricatorAuthLoginController' => 'PhabricatorAuthController',
- 'PhabricatorAuthLoginHandler' => 'Phobject',
'PhabricatorAuthLoginMessageType' => 'PhabricatorAuthMessageType',
'PhabricatorAuthLogoutConduitAPIMethod' => 'PhabricatorAuthConduitAPIMethod',
'PhabricatorAuthMFAEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorAuthMFASyncTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
'PhabricatorAuthMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension',
'PhabricatorAuthManagementCachePKCS8Workflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementLDAPWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementListFactorsWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementListMFAProvidersWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementRecoverWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementRefreshWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementRevokeWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementStripWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementTrustOAuthClientWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementUnlimitWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementUntrustOAuthClientWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementVerifyWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorAuthMessage' => array(
'PhabricatorAuthDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorAuthMessageController' => 'PhabricatorAuthProviderController',
'PhabricatorAuthMessageEditController' => 'PhabricatorAuthMessageController',
'PhabricatorAuthMessageEditEngine' => 'PhabricatorEditEngine',
'PhabricatorAuthMessageEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorAuthMessageListController' => 'PhabricatorAuthProviderController',
'PhabricatorAuthMessagePHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthMessageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthMessageTextTransaction' => 'PhabricatorAuthMessageTransactionType',
'PhabricatorAuthMessageTransaction' => 'PhabricatorModularTransaction',
'PhabricatorAuthMessageTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorAuthMessageTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorAuthMessageType' => 'Phobject',
'PhabricatorAuthMessageViewController' => 'PhabricatorAuthMessageController',
'PhabricatorAuthNeedsApprovalController' => 'PhabricatorAuthController',
'PhabricatorAuthNeedsMultiFactorController' => 'PhabricatorAuthController',
'PhabricatorAuthNewController' => 'PhabricatorAuthProviderConfigController',
'PhabricatorAuthNewFactorAction' => 'PhabricatorSystemAction',
'PhabricatorAuthOldOAuthRedirectController' => 'PhabricatorAuthController',
'PhabricatorAuthOneTimeLoginController' => 'PhabricatorAuthController',
'PhabricatorAuthOneTimeLoginTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
'PhabricatorAuthPassword' => array(
'PhabricatorAuthDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhabricatorAuthPasswordEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorAuthPasswordEngine' => 'Phobject',
'PhabricatorAuthPasswordException' => 'Exception',
'PhabricatorAuthPasswordPHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthPasswordQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthPasswordResetTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
'PhabricatorAuthPasswordRevokeTransaction' => 'PhabricatorAuthPasswordTransactionType',
'PhabricatorAuthPasswordRevoker' => 'PhabricatorAuthRevoker',
'PhabricatorAuthPasswordTestCase' => 'PhabricatorTestCase',
'PhabricatorAuthPasswordTransaction' => 'PhabricatorModularTransaction',
'PhabricatorAuthPasswordTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorAuthPasswordTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorAuthPasswordUpgradeTransaction' => 'PhabricatorAuthPasswordTransactionType',
'PhabricatorAuthProvider' => 'Phobject',
'PhabricatorAuthProviderConfig' => array(
'PhabricatorAuthDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'PhabricatorAuthProviderConfigController' => 'PhabricatorAuthProviderController',
'PhabricatorAuthProviderConfigEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorAuthProviderConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthProviderConfigTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorAuthProviderConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorAuthProviderController' => 'PhabricatorAuthController',
+ 'PhabricatorAuthProviderViewController' => 'PhabricatorAuthProviderConfigController',
'PhabricatorAuthProvidersGuidanceContext' => 'PhabricatorGuidanceContext',
'PhabricatorAuthProvidersGuidanceEngineExtension' => 'PhabricatorGuidanceEngineExtension',
'PhabricatorAuthQueryPublicKeysConduitAPIMethod' => 'PhabricatorAuthConduitAPIMethod',
'PhabricatorAuthRegisterController' => 'PhabricatorAuthController',
'PhabricatorAuthRevokeTokenController' => 'PhabricatorAuthController',
'PhabricatorAuthRevoker' => 'Phobject',
'PhabricatorAuthSSHKey' => array(
'PhabricatorAuthDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhabricatorAuthSSHKeyController' => 'PhabricatorAuthController',
'PhabricatorAuthSSHKeyEditController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHKeyEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorAuthSSHKeyGenerateController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHKeyListController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHKeyPHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthSSHKeyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthSSHKeyReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorAuthSSHKeyRevokeController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHKeySearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorAuthSSHKeyTableView' => 'AphrontView',
'PhabricatorAuthSSHKeyTestCase' => 'PhabricatorTestCase',
'PhabricatorAuthSSHKeyTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorAuthSSHKeyTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorAuthSSHKeyViewController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHPublicKey' => 'Phobject',
'PhabricatorAuthSSHRevoker' => 'PhabricatorAuthRevoker',
'PhabricatorAuthSession' => array(
'PhabricatorAuthDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorAuthSessionEngine' => 'Phobject',
'PhabricatorAuthSessionEngineExtension' => 'Phobject',
'PhabricatorAuthSessionEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorAuthSessionGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorAuthSessionInfo' => 'Phobject',
'PhabricatorAuthSessionPHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthSessionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthSessionRevoker' => 'PhabricatorAuthRevoker',
+ 'PhabricatorAuthSetExternalController' => 'PhabricatorAuthController',
'PhabricatorAuthSetPasswordController' => 'PhabricatorAuthController',
'PhabricatorAuthSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorAuthStartController' => 'PhabricatorAuthController',
'PhabricatorAuthTemporaryToken' => array(
'PhabricatorAuthDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorAuthTemporaryTokenGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorAuthTemporaryTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthTemporaryTokenRevoker' => 'PhabricatorAuthRevoker',
'PhabricatorAuthTemporaryTokenType' => 'Phobject',
'PhabricatorAuthTemporaryTokenTypeModule' => 'PhabricatorConfigModule',
'PhabricatorAuthTerminateSessionController' => 'PhabricatorAuthController',
'PhabricatorAuthTestSMSAction' => 'PhabricatorSystemAction',
'PhabricatorAuthTryFactorAction' => 'PhabricatorSystemAction',
'PhabricatorAuthUnlinkController' => 'PhabricatorAuthController',
'PhabricatorAuthValidateController' => 'PhabricatorAuthController',
'PhabricatorAuthWelcomeMailMessageType' => 'PhabricatorAuthMessageType',
'PhabricatorAuthenticationConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorAutoEventListener' => 'PhabricatorEventListener',
'PhabricatorBadgesApplication' => 'PhabricatorApplication',
'PhabricatorBadgesArchiveController' => 'PhabricatorBadgesController',
'PhabricatorBadgesAward' => array(
'PhabricatorBadgesDAO',
'PhabricatorDestructibleInterface',
'PhabricatorPolicyInterface',
),
'PhabricatorBadgesAwardController' => 'PhabricatorBadgesController',
'PhabricatorBadgesAwardQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorBadgesAwardTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorBadgesBadge' => array(
'PhabricatorBadgesDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorSubscribableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorBadgesBadgeAwardTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeDescriptionTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeFlavorTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeIconTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeNameNgrams' => 'PhabricatorSearchNgrams',
'PhabricatorBadgesBadgeNameTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeQualityTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeRevokeTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeStatusTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorBadgesBadgeTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorBadgesCommentController' => 'PhabricatorBadgesController',
'PhabricatorBadgesController' => 'PhabricatorController',
'PhabricatorBadgesCreateCapability' => 'PhabricatorPolicyCapability',
'PhabricatorBadgesDAO' => 'PhabricatorLiskDAO',
'PhabricatorBadgesDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorBadgesDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PhabricatorBadgesEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhabricatorBadgesEditController' => 'PhabricatorBadgesController',
'PhabricatorBadgesEditEngine' => 'PhabricatorEditEngine',
'PhabricatorBadgesEditRecipientsController' => 'PhabricatorBadgesController',
'PhabricatorBadgesEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorBadgesIconSet' => 'PhabricatorIconSet',
'PhabricatorBadgesListController' => 'PhabricatorBadgesController',
'PhabricatorBadgesLootContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhabricatorBadgesMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorBadgesPHIDType' => 'PhabricatorPHIDType',
'PhabricatorBadgesProfileController' => 'PhabricatorController',
'PhabricatorBadgesQuality' => 'Phobject',
'PhabricatorBadgesQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorBadgesRecipientsController' => 'PhabricatorBadgesProfileController',
'PhabricatorBadgesRecipientsListView' => 'AphrontView',
'PhabricatorBadgesRemoveRecipientsController' => 'PhabricatorBadgesController',
'PhabricatorBadgesReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorBadgesSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorBadgesSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorBadgesSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorBadgesTransaction' => 'PhabricatorModularTransaction',
'PhabricatorBadgesTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorBadgesTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorBadgesViewController' => 'PhabricatorBadgesProfileController',
'PhabricatorBarePageView' => 'AphrontPageView',
'PhabricatorBaseURISetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorBcryptPasswordHasher' => 'PhabricatorPasswordHasher',
'PhabricatorBinariesSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorBitbucketAuthProvider' => 'PhabricatorOAuth1AuthProvider',
'PhabricatorBoardColumnsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorBoardLayoutEngine' => 'Phobject',
'PhabricatorBoardRenderingEngine' => 'Phobject',
'PhabricatorBoardResponseEngine' => 'Phobject',
'PhabricatorBoolConfigType' => 'PhabricatorTextConfigType',
'PhabricatorBoolEditField' => 'PhabricatorEditField',
'PhabricatorBoolMailStamp' => 'PhabricatorMailStamp',
'PhabricatorBritishEnglishTranslation' => 'PhutilTranslation',
'PhabricatorBuiltinDraftEngine' => 'PhabricatorDraftEngine',
'PhabricatorBuiltinFileCachePurger' => 'PhabricatorCachePurger',
'PhabricatorBuiltinPatchList' => 'PhabricatorSQLPatchList',
'PhabricatorBulkContentSource' => 'PhabricatorContentSource',
'PhabricatorBulkEditGroup' => 'Phobject',
'PhabricatorBulkEngine' => 'Phobject',
'PhabricatorBulkManagementExportWorkflow' => 'PhabricatorBulkManagementWorkflow',
'PhabricatorBulkManagementMakeSilentWorkflow' => 'PhabricatorBulkManagementWorkflow',
'PhabricatorBulkManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorCSVExportFormat' => 'PhabricatorExportFormat',
'PhabricatorCacheDAO' => 'PhabricatorLiskDAO',
'PhabricatorCacheEngine' => 'Phobject',
'PhabricatorCacheEngineExtension' => 'Phobject',
'PhabricatorCacheGeneralGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorCacheManagementPurgeWorkflow' => 'PhabricatorCacheManagementWorkflow',
'PhabricatorCacheManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorCacheMarkupGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorCachePurger' => 'Phobject',
'PhabricatorCacheSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorCacheSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorCacheSpec' => 'Phobject',
'PhabricatorCacheTTLGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorCachedClassMapQuery' => 'Phobject',
'PhabricatorCaches' => 'Phobject',
'PhabricatorCachesTestCase' => 'PhabricatorTestCase',
'PhabricatorCalendarApplication' => 'PhabricatorApplication',
'PhabricatorCalendarController' => 'PhabricatorController',
'PhabricatorCalendarDAO' => 'PhabricatorLiskDAO',
'PhabricatorCalendarEvent' => array(
'PhabricatorCalendarDAO',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorPolicyCodexInterface',
'PhabricatorProjectInterface',
'PhabricatorMarkupInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorSubscribableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorDestructibleInterface',
'PhabricatorMentionableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorSpacesInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'PhabricatorConduitResultInterface',
),
'PhabricatorCalendarEventAcceptTransaction' => 'PhabricatorCalendarEventReplyTransaction',
'PhabricatorCalendarEventAllDayTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventAvailabilityController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventCancelController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventCancelTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventDateTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventDeclineTransaction' => 'PhabricatorCalendarEventReplyTransaction',
'PhabricatorCalendarEventDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PhabricatorCalendarEventDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PhabricatorCalendarEventDescriptionTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventDragController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhabricatorCalendarEventEditController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventEditEngine' => 'PhabricatorEditEngine',
'PhabricatorCalendarEventEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorCalendarEventEmailCommand' => 'MetaMTAEmailTransactionCommand',
'PhabricatorCalendarEventEndDateTransaction' => 'PhabricatorCalendarEventDateTransaction',
'PhabricatorCalendarEventExportController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventFerretEngine' => 'PhabricatorFerretEngine',
'PhabricatorCalendarEventForkTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventFrequencyTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventFulltextEngine' => 'PhabricatorFulltextEngine',
'PhabricatorCalendarEventHeraldAdapter' => 'HeraldAdapter',
'PhabricatorCalendarEventHeraldField' => 'HeraldField',
'PhabricatorCalendarEventHeraldFieldGroup' => 'HeraldFieldGroup',
'PhabricatorCalendarEventHostPolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorCalendarEventHostTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventIconTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventInviteTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventInvitee' => array(
'PhabricatorCalendarDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorCalendarEventInviteeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCalendarEventInviteesPolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorCalendarEventJoinController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventListController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorCalendarEventNameHeraldField' => 'PhabricatorCalendarEventHeraldField',
'PhabricatorCalendarEventNameTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventNotificationView' => 'Phobject',
'PhabricatorCalendarEventPHIDType' => 'PhabricatorPHIDType',
'PhabricatorCalendarEventPolicyCodex' => 'PhabricatorPolicyCodex',
'PhabricatorCalendarEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCalendarEventRSVPEmailCommand' => 'PhabricatorCalendarEventEmailCommand',
'PhabricatorCalendarEventRecurringTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventReplyTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorCalendarEventSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorCalendarEventStartDateTransaction' => 'PhabricatorCalendarEventDateTransaction',
'PhabricatorCalendarEventTransaction' => 'PhabricatorModularTransaction',
'PhabricatorCalendarEventTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorCalendarEventTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorCalendarEventTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorCalendarEventUntilDateTransaction' => 'PhabricatorCalendarEventDateTransaction',
'PhabricatorCalendarEventViewController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExport' => array(
'PhabricatorCalendarDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorCalendarExportDisableController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExportDisableTransaction' => 'PhabricatorCalendarExportTransactionType',
'PhabricatorCalendarExportEditController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExportEditEngine' => 'PhabricatorEditEngine',
'PhabricatorCalendarExportEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorCalendarExportICSController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExportListController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExportModeTransaction' => 'PhabricatorCalendarExportTransactionType',
'PhabricatorCalendarExportNameTransaction' => 'PhabricatorCalendarExportTransactionType',
'PhabricatorCalendarExportPHIDType' => 'PhabricatorPHIDType',
'PhabricatorCalendarExportQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCalendarExportQueryKeyTransaction' => 'PhabricatorCalendarExportTransactionType',
'PhabricatorCalendarExportSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorCalendarExportTransaction' => 'PhabricatorModularTransaction',
'PhabricatorCalendarExportTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorCalendarExportTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorCalendarExportViewController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExternalInvitee' => array(
'PhabricatorCalendarDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorCalendarExternalInviteePHIDType' => 'PhabricatorPHIDType',
'PhabricatorCalendarExternalInviteeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCalendarICSFileImportEngine' => 'PhabricatorCalendarICSImportEngine',
'PhabricatorCalendarICSImportEngine' => 'PhabricatorCalendarImportEngine',
'PhabricatorCalendarICSURIImportEngine' => 'PhabricatorCalendarICSImportEngine',
'PhabricatorCalendarICSWriter' => 'Phobject',
'PhabricatorCalendarIconSet' => 'PhabricatorIconSet',
'PhabricatorCalendarImport' => array(
'PhabricatorCalendarDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorCalendarImportDefaultLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportDeleteController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportDeleteLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportDeleteTransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportDisableController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportDisableTransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportDropController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportDuplicateLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportEditController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportEditEngine' => 'PhabricatorEditEngine',
'PhabricatorCalendarImportEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorCalendarImportEmptyLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportEngine' => 'Phobject',
'PhabricatorCalendarImportEpochLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportFetchLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportFrequencyLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportFrequencyTransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportICSFileTransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportICSLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportICSURITransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportICSWarningLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportIgnoredNodeLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportListController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportLog' => array(
'PhabricatorCalendarDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorCalendarImportLogListController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCalendarImportLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorCalendarImportLogType' => 'Phobject',
'PhabricatorCalendarImportLogView' => 'AphrontView',
'PhabricatorCalendarImportNameTransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportOriginalLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportOrphanLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportPHIDType' => 'PhabricatorPHIDType',
'PhabricatorCalendarImportQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCalendarImportQueueLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportReloadController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportReloadTransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportReloadWorker' => 'PhabricatorWorker',
'PhabricatorCalendarImportSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorCalendarImportTransaction' => 'PhabricatorModularTransaction',
'PhabricatorCalendarImportTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorCalendarImportTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorCalendarImportTriggerLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportUpdateLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportViewController' => 'PhabricatorCalendarController',
'PhabricatorCalendarInviteeDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorCalendarInviteeUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorCalendarInviteeViewerFunctionDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorCalendarManagementNotifyWorkflow' => 'PhabricatorCalendarManagementWorkflow',
'PhabricatorCalendarManagementReloadWorkflow' => 'PhabricatorCalendarManagementWorkflow',
'PhabricatorCalendarManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorCalendarNotification' => 'PhabricatorCalendarDAO',
'PhabricatorCalendarNotificationEngine' => 'Phobject',
'PhabricatorCalendarRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorCalendarReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorCalendarSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorCelerityApplication' => 'PhabricatorApplication',
'PhabricatorCelerityTestCase' => 'PhabricatorTestCase',
'PhabricatorChangeParserTestCase' => 'PhabricatorWorkingCopyTestCase',
'PhabricatorChangesetCachePurger' => 'PhabricatorCachePurger',
'PhabricatorChangesetResponse' => 'AphrontProxyResponse',
'PhabricatorChatLogApplication' => 'PhabricatorApplication',
'PhabricatorChatLogChannel' => array(
'PhabricatorChatLogDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorChatLogChannelListController' => 'PhabricatorChatLogController',
'PhabricatorChatLogChannelLogController' => 'PhabricatorChatLogController',
'PhabricatorChatLogChannelQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorChatLogController' => 'PhabricatorController',
'PhabricatorChatLogDAO' => 'PhabricatorLiskDAO',
'PhabricatorChatLogEvent' => array(
'PhabricatorChatLogDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorChatLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCheckboxesEditField' => 'PhabricatorEditField',
'PhabricatorChunkedFileStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorClassConfigType' => 'PhabricatorTextConfigType',
'PhabricatorClusterConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorClusterDatabasesConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorClusterException' => 'Exception',
'PhabricatorClusterExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorClusterImpossibleWriteException' => 'PhabricatorClusterException',
'PhabricatorClusterImproperWriteException' => 'PhabricatorClusterException',
'PhabricatorClusterMailersConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorClusterNoHostForRoleException' => 'Exception',
'PhabricatorClusterSearchConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorClusterServiceHealthRecord' => 'Phobject',
'PhabricatorClusterStrandedException' => 'PhabricatorClusterException',
'PhabricatorColumnsEditField' => 'PhabricatorPHIDListEditField',
'PhabricatorCommentEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorCommentEditField' => 'PhabricatorEditField',
'PhabricatorCommentEditType' => 'PhabricatorEditType',
'PhabricatorCommitBranchesField' => 'PhabricatorCommitCustomField',
'PhabricatorCommitCustomField' => 'PhabricatorCustomField',
'PhabricatorCommitMergedCommitsField' => 'PhabricatorCommitCustomField',
'PhabricatorCommitRepositoryField' => 'PhabricatorCommitCustomField',
'PhabricatorCommitSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorCommitTagsField' => 'PhabricatorCommitCustomField',
'PhabricatorCommonPasswords' => 'Phobject',
'PhabricatorConduitAPIController' => 'PhabricatorConduitController',
'PhabricatorConduitApplication' => 'PhabricatorApplication',
'PhabricatorConduitCallManagementWorkflow' => 'PhabricatorConduitManagementWorkflow',
'PhabricatorConduitCertificateToken' => 'PhabricatorConduitDAO',
'PhabricatorConduitConsoleController' => 'PhabricatorConduitController',
'PhabricatorConduitContentSource' => 'PhabricatorContentSource',
'PhabricatorConduitController' => 'PhabricatorController',
'PhabricatorConduitDAO' => 'PhabricatorLiskDAO',
'PhabricatorConduitEditField' => 'PhabricatorEditField',
'PhabricatorConduitListController' => 'PhabricatorConduitController',
'PhabricatorConduitLogController' => 'PhabricatorConduitController',
'PhabricatorConduitLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorConduitLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorConduitManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorConduitMethodCallLog' => array(
'PhabricatorConduitDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorConduitMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorConduitRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorConduitResultInterface' => 'PhabricatorPHIDInterface',
'PhabricatorConduitSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorConduitSearchFieldSpecification' => 'Phobject',
'PhabricatorConduitTestCase' => 'PhabricatorTestCase',
'PhabricatorConduitToken' => array(
'PhabricatorConduitDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorConduitTokenController' => 'PhabricatorConduitController',
'PhabricatorConduitTokenEditController' => 'PhabricatorConduitController',
'PhabricatorConduitTokenHandshakeController' => 'PhabricatorConduitController',
'PhabricatorConduitTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorConduitTokenTerminateController' => 'PhabricatorConduitController',
'PhabricatorConduitTokensSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorConfigAllController' => 'PhabricatorConfigController',
'PhabricatorConfigApplication' => 'PhabricatorApplication',
'PhabricatorConfigApplicationController' => 'PhabricatorConfigController',
'PhabricatorConfigCacheController' => 'PhabricatorConfigController',
'PhabricatorConfigClusterDatabasesController' => 'PhabricatorConfigController',
'PhabricatorConfigClusterNotificationsController' => 'PhabricatorConfigController',
'PhabricatorConfigClusterRepositoriesController' => 'PhabricatorConfigController',
'PhabricatorConfigClusterSearchController' => 'PhabricatorConfigController',
'PhabricatorConfigCollectorsModule' => 'PhabricatorConfigModule',
'PhabricatorConfigColumnSchema' => 'PhabricatorConfigStorageSchema',
'PhabricatorConfigConfigPHIDType' => 'PhabricatorPHIDType',
'PhabricatorConfigConstants' => 'Phobject',
'PhabricatorConfigController' => 'PhabricatorController',
'PhabricatorConfigCoreSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorConfigDatabaseController' => 'PhabricatorConfigController',
'PhabricatorConfigDatabaseIssueController' => 'PhabricatorConfigDatabaseController',
'PhabricatorConfigDatabaseSchema' => 'PhabricatorConfigStorageSchema',
'PhabricatorConfigDatabaseSource' => 'PhabricatorConfigProxySource',
'PhabricatorConfigDatabaseStatusController' => 'PhabricatorConfigDatabaseController',
'PhabricatorConfigDefaultSource' => 'PhabricatorConfigProxySource',
'PhabricatorConfigDictionarySource' => 'PhabricatorConfigSource',
'PhabricatorConfigEdgeModule' => 'PhabricatorConfigModule',
'PhabricatorConfigEditController' => 'PhabricatorConfigController',
'PhabricatorConfigEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorConfigEntry' => array(
'PhabricatorConfigEntryDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'PhabricatorConfigEntryDAO' => 'PhabricatorLiskDAO',
'PhabricatorConfigEntryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorConfigFileSource' => 'PhabricatorConfigProxySource',
'PhabricatorConfigGroupConstants' => 'PhabricatorConfigConstants',
'PhabricatorConfigGroupController' => 'PhabricatorConfigController',
'PhabricatorConfigHTTPParameterTypesModule' => 'PhabricatorConfigModule',
'PhabricatorConfigHistoryController' => 'PhabricatorConfigController',
'PhabricatorConfigIgnoreController' => 'PhabricatorConfigController',
'PhabricatorConfigIssueListController' => 'PhabricatorConfigController',
'PhabricatorConfigIssuePanelController' => 'PhabricatorConfigController',
'PhabricatorConfigIssueViewController' => 'PhabricatorConfigController',
'PhabricatorConfigJSON' => 'Phobject',
'PhabricatorConfigJSONOptionType' => 'PhabricatorConfigOptionType',
'PhabricatorConfigKeySchema' => 'PhabricatorConfigStorageSchema',
'PhabricatorConfigListController' => 'PhabricatorConfigController',
'PhabricatorConfigLocalSource' => 'PhabricatorConfigProxySource',
'PhabricatorConfigManagementDeleteWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementDoneWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementGetWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementListWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementMigrateWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementSetWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorConfigManualActivity' => 'PhabricatorConfigEntryDAO',
'PhabricatorConfigModule' => 'Phobject',
'PhabricatorConfigModuleController' => 'PhabricatorConfigController',
'PhabricatorConfigOption' => 'Phobject',
'PhabricatorConfigOptionType' => 'Phobject',
'PhabricatorConfigPHIDModule' => 'PhabricatorConfigModule',
'PhabricatorConfigProxySource' => 'PhabricatorConfigSource',
'PhabricatorConfigPurgeCacheController' => 'PhabricatorConfigController',
'PhabricatorConfigRegexOptionType' => 'PhabricatorConfigJSONOptionType',
'PhabricatorConfigRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorConfigRequestExceptionHandlerModule' => 'PhabricatorConfigModule',
'PhabricatorConfigResponse' => 'AphrontStandaloneHTMLResponse',
'PhabricatorConfigSchemaQuery' => 'Phobject',
'PhabricatorConfigSchemaSpec' => 'Phobject',
'PhabricatorConfigServerSchema' => 'PhabricatorConfigStorageSchema',
'PhabricatorConfigSetupCheckModule' => 'PhabricatorConfigModule',
'PhabricatorConfigSiteModule' => 'PhabricatorConfigModule',
'PhabricatorConfigSiteSource' => 'PhabricatorConfigProxySource',
'PhabricatorConfigSource' => 'Phobject',
'PhabricatorConfigStackSource' => 'PhabricatorConfigSource',
'PhabricatorConfigStorageSchema' => 'Phobject',
'PhabricatorConfigTableSchema' => 'PhabricatorConfigStorageSchema',
'PhabricatorConfigTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorConfigType' => 'Phobject',
'PhabricatorConfigValidationException' => 'Exception',
'PhabricatorConfigVersionController' => 'PhabricatorConfigController',
'PhabricatorConpherenceApplication' => 'PhabricatorApplication',
'PhabricatorConpherenceColumnMinimizeSetting' => 'PhabricatorInternalSetting',
'PhabricatorConpherenceColumnVisibleSetting' => 'PhabricatorInternalSetting',
'PhabricatorConpherenceNotificationsSetting' => 'PhabricatorSelectSetting',
'PhabricatorConpherencePreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorConpherenceProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorConpherenceRoomContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhabricatorConpherenceRoomTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorConpherenceSoundSetting' => 'PhabricatorSelectSetting',
'PhabricatorConpherenceThreadPHIDType' => 'PhabricatorPHIDType',
'PhabricatorConpherenceWidgetVisibleSetting' => 'PhabricatorInternalSetting',
'PhabricatorConsoleApplication' => 'PhabricatorApplication',
'PhabricatorConsoleContentSource' => 'PhabricatorContentSource',
'PhabricatorContactNumbersSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorContentSource' => 'Phobject',
'PhabricatorContentSourceModule' => 'PhabricatorConfigModule',
'PhabricatorContentSourceView' => 'AphrontView',
'PhabricatorContributedToObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorController' => 'AphrontController',
'PhabricatorCookies' => 'Phobject',
'PhabricatorCoreConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorCoreCreateTransaction' => 'PhabricatorCoreTransactionType',
'PhabricatorCoreTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorCoreVoidTransaction' => 'PhabricatorModularTransactionType',
'PhabricatorCountFact' => 'PhabricatorFact',
'PhabricatorCountdown' => array(
'PhabricatorCountdownDAO',
'PhabricatorPolicyInterface',
'PhabricatorFlaggableInterface',
'PhabricatorSubscribableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorSpacesInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
),
'PhabricatorCountdownApplication' => 'PhabricatorApplication',
'PhabricatorCountdownController' => 'PhabricatorController',
'PhabricatorCountdownCountdownPHIDType' => 'PhabricatorPHIDType',
'PhabricatorCountdownDAO' => 'PhabricatorLiskDAO',
'PhabricatorCountdownDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PhabricatorCountdownDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PhabricatorCountdownDescriptionTransaction' => 'PhabricatorCountdownTransactionType',
'PhabricatorCountdownEditController' => 'PhabricatorCountdownController',
'PhabricatorCountdownEditEngine' => 'PhabricatorEditEngine',
'PhabricatorCountdownEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorCountdownEpochTransaction' => 'PhabricatorCountdownTransactionType',
'PhabricatorCountdownListController' => 'PhabricatorCountdownController',
'PhabricatorCountdownMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorCountdownQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCountdownRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorCountdownReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorCountdownSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorCountdownSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorCountdownTitleTransaction' => 'PhabricatorCountdownTransactionType',
'PhabricatorCountdownTransaction' => 'PhabricatorModularTransaction',
'PhabricatorCountdownTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorCountdownTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorCountdownTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorCountdownView' => 'AphrontView',
'PhabricatorCountdownViewController' => 'PhabricatorCountdownController',
'PhabricatorCredentialEditField' => 'PhabricatorEditField',
'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery',
'PhabricatorCustomField' => 'Phobject',
'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorCustomFieldApplicationSearchDatasource' => 'PhabricatorTypeaheadProxyDatasource',
'PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorCustomFieldAttachment' => 'Phobject',
'PhabricatorCustomFieldConfigOptionType' => 'PhabricatorConfigOptionType',
'PhabricatorCustomFieldDataNotAvailableException' => 'Exception',
'PhabricatorCustomFieldEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorCustomFieldEditField' => 'PhabricatorEditField',
'PhabricatorCustomFieldEditType' => 'PhabricatorEditType',
'PhabricatorCustomFieldExportEngineExtension' => 'PhabricatorExportEngineExtension',
'PhabricatorCustomFieldFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension',
'PhabricatorCustomFieldHeraldAction' => 'HeraldAction',
'PhabricatorCustomFieldHeraldActionGroup' => 'HeraldActionGroup',
'PhabricatorCustomFieldHeraldField' => 'HeraldField',
'PhabricatorCustomFieldHeraldFieldGroup' => 'HeraldFieldGroup',
'PhabricatorCustomFieldImplementationIncompleteException' => 'Exception',
'PhabricatorCustomFieldIndexStorage' => 'PhabricatorLiskDAO',
'PhabricatorCustomFieldList' => 'Phobject',
'PhabricatorCustomFieldMonogramParser' => 'Phobject',
'PhabricatorCustomFieldNotAttachedException' => 'Exception',
'PhabricatorCustomFieldNotProxyException' => 'Exception',
'PhabricatorCustomFieldNumericIndexStorage' => 'PhabricatorCustomFieldIndexStorage',
'PhabricatorCustomFieldSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorCustomFieldStorage' => 'PhabricatorLiskDAO',
'PhabricatorCustomFieldStorageQuery' => 'Phobject',
'PhabricatorCustomFieldStringIndexStorage' => 'PhabricatorCustomFieldIndexStorage',
'PhabricatorCustomLogoConfigType' => 'PhabricatorConfigOptionType',
'PhabricatorCustomUIFooterConfigType' => 'PhabricatorConfigJSONOptionType',
'PhabricatorDaemon' => 'PhutilDaemon',
'PhabricatorDaemonBulkJobController' => 'PhabricatorDaemonController',
'PhabricatorDaemonBulkJobListController' => 'PhabricatorDaemonBulkJobController',
'PhabricatorDaemonBulkJobMonitorController' => 'PhabricatorDaemonBulkJobController',
'PhabricatorDaemonBulkJobViewController' => 'PhabricatorDaemonBulkJobController',
'PhabricatorDaemonConsoleController' => 'PhabricatorDaemonController',
'PhabricatorDaemonContentSource' => 'PhabricatorContentSource',
'PhabricatorDaemonController' => 'PhabricatorController',
'PhabricatorDaemonDAO' => 'PhabricatorLiskDAO',
'PhabricatorDaemonEventListener' => 'PhabricatorEventListener',
'PhabricatorDaemonLockLog' => 'PhabricatorDaemonDAO',
'PhabricatorDaemonLockLogGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorDaemonLog' => array(
'PhabricatorDaemonDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorDaemonLogEvent' => 'PhabricatorDaemonDAO',
'PhabricatorDaemonLogEventGarbageCollector' => 'PhabricatorGarbageCollector',
- 'PhabricatorDaemonLogEventViewController' => 'PhabricatorDaemonController',
- 'PhabricatorDaemonLogEventsView' => 'AphrontView',
'PhabricatorDaemonLogGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorDaemonLogListController' => 'PhabricatorDaemonController',
'PhabricatorDaemonLogListView' => 'AphrontView',
'PhabricatorDaemonLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorDaemonLogViewController' => 'PhabricatorDaemonController',
'PhabricatorDaemonManagementDebugWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementLaunchWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementListWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementLogWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementReloadWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementRestartWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementStartWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementStatusWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementStopWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorDaemonOverseerModule' => 'PhutilDaemonOverseerModule',
'PhabricatorDaemonReference' => 'Phobject',
'PhabricatorDaemonTaskGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorDaemonTasksTableView' => 'AphrontView',
'PhabricatorDaemonsApplication' => 'PhabricatorApplication',
'PhabricatorDaemonsSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorDailyRoutineTriggerClock' => 'PhabricatorTriggerClock',
'PhabricatorDarkConsoleSetting' => 'PhabricatorSelectSetting',
'PhabricatorDarkConsoleTabSetting' => 'PhabricatorInternalSetting',
'PhabricatorDarkConsoleVisibleSetting' => 'PhabricatorInternalSetting',
'PhabricatorDashboard' => array(
'PhabricatorDashboardDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorFlaggableInterface',
'PhabricatorDestructibleInterface',
'PhabricatorProjectInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorDashboardAddPanelController' => 'PhabricatorDashboardController',
'PhabricatorDashboardApplication' => 'PhabricatorApplication',
'PhabricatorDashboardArchiveController' => 'PhabricatorDashboardController',
'PhabricatorDashboardArrangeController' => 'PhabricatorDashboardProfileController',
'PhabricatorDashboardController' => 'PhabricatorController',
'PhabricatorDashboardDAO' => 'PhabricatorLiskDAO',
'PhabricatorDashboardDashboardHasPanelEdgeType' => 'PhabricatorEdgeType',
'PhabricatorDashboardDashboardPHIDType' => 'PhabricatorPHIDType',
'PhabricatorDashboardDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorDashboardEditController' => 'PhabricatorDashboardController',
'PhabricatorDashboardIconSet' => 'PhabricatorIconSet',
'PhabricatorDashboardInstall' => 'PhabricatorDashboardDAO',
'PhabricatorDashboardInstallController' => 'PhabricatorDashboardController',
'PhabricatorDashboardLayoutConfig' => 'Phobject',
'PhabricatorDashboardListController' => 'PhabricatorDashboardController',
'PhabricatorDashboardManageController' => 'PhabricatorDashboardProfileController',
'PhabricatorDashboardMovePanelController' => 'PhabricatorDashboardController',
'PhabricatorDashboardNgrams' => 'PhabricatorSearchNgrams',
'PhabricatorDashboardPanel' => array(
'PhabricatorDashboardDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorFlaggableInterface',
'PhabricatorDestructibleInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorDashboardPanelArchiveController' => 'PhabricatorDashboardController',
'PhabricatorDashboardPanelCoreCustomField' => array(
'PhabricatorDashboardPanelCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'PhabricatorDashboardPanelCustomField' => 'PhabricatorCustomField',
'PhabricatorDashboardPanelDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorDashboardPanelEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhabricatorDashboardPanelEditController' => 'PhabricatorDashboardController',
'PhabricatorDashboardPanelEditEngine' => 'PhabricatorEditEngine',
'PhabricatorDashboardPanelEditproController' => 'PhabricatorDashboardController',
'PhabricatorDashboardPanelHasDashboardEdgeType' => 'PhabricatorEdgeType',
'PhabricatorDashboardPanelListController' => 'PhabricatorDashboardController',
'PhabricatorDashboardPanelNgrams' => 'PhabricatorSearchNgrams',
'PhabricatorDashboardPanelPHIDType' => 'PhabricatorPHIDType',
'PhabricatorDashboardPanelQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorDashboardPanelRenderController' => 'PhabricatorDashboardController',
'PhabricatorDashboardPanelRenderingEngine' => 'Phobject',
'PhabricatorDashboardPanelSearchApplicationCustomField' => 'PhabricatorStandardCustomField',
'PhabricatorDashboardPanelSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorDashboardPanelSearchQueryCustomField' => 'PhabricatorStandardCustomField',
'PhabricatorDashboardPanelTabsCustomField' => 'PhabricatorStandardCustomField',
'PhabricatorDashboardPanelTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorDashboardPanelTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorDashboardPanelTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorDashboardPanelType' => 'Phobject',
'PhabricatorDashboardPanelViewController' => 'PhabricatorDashboardController',
'PhabricatorDashboardProfileController' => 'PhabricatorController',
'PhabricatorDashboardProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorDashboardQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorDashboardQueryPanelInstallController' => 'PhabricatorDashboardController',
'PhabricatorDashboardQueryPanelType' => 'PhabricatorDashboardPanelType',
'PhabricatorDashboardRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorDashboardRemovePanelController' => 'PhabricatorDashboardController',
'PhabricatorDashboardRenderingEngine' => 'Phobject',
'PhabricatorDashboardSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorDashboardSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorDashboardTabsPanelType' => 'PhabricatorDashboardPanelType',
'PhabricatorDashboardTextPanelType' => 'PhabricatorDashboardPanelType',
'PhabricatorDashboardTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorDashboardTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorDashboardTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorDashboardViewController' => 'PhabricatorDashboardProfileController',
'PhabricatorDataCacheSpec' => 'PhabricatorCacheSpec',
'PhabricatorDataNotAttachedException' => 'Exception',
'PhabricatorDatabaseRef' => 'Phobject',
'PhabricatorDatabaseRefParser' => 'Phobject',
'PhabricatorDatabaseSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorDatasourceApplicationEngineExtension' => 'PhabricatorDatasourceEngineExtension',
'PhabricatorDatasourceEditField' => 'PhabricatorTokenizerEditField',
'PhabricatorDatasourceEditType' => 'PhabricatorPHIDListEditType',
'PhabricatorDatasourceEngine' => 'Phobject',
'PhabricatorDatasourceEngineExtension' => 'Phobject',
'PhabricatorDateFormatSetting' => 'PhabricatorSelectSetting',
'PhabricatorDateTimeSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorDebugController' => 'PhabricatorController',
'PhabricatorDefaultRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorDefaultSyntaxStyle' => 'PhabricatorSyntaxStyle',
+ 'PhabricatorDefaultUnlockEngine' => 'PhabricatorUnlockEngine',
'PhabricatorDestructibleCodex' => 'Phobject',
'PhabricatorDestructionEngine' => 'Phobject',
'PhabricatorDestructionEngineExtension' => 'Phobject',
'PhabricatorDestructionEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorDeveloperConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorDeveloperPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorDiffInlineCommentQuery' => 'PhabricatorApplicationTransactionCommentQuery',
'PhabricatorDiffPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
+ 'PhabricatorDiffScopeEngine' => 'Phobject',
+ 'PhabricatorDiffScopeEngineTestCase' => 'PhabricatorTestCase',
'PhabricatorDifferenceEngine' => 'Phobject',
'PhabricatorDifferentialApplication' => 'PhabricatorApplication',
'PhabricatorDifferentialAttachCommitWorkflow' => 'PhabricatorDifferentialManagementWorkflow',
'PhabricatorDifferentialConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorDifferentialExtractWorkflow' => 'PhabricatorDifferentialManagementWorkflow',
'PhabricatorDifferentialManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorDifferentialMigrateHunkWorkflow' => 'PhabricatorDifferentialManagementWorkflow',
'PhabricatorDifferentialRebuildChangesetsWorkflow' => 'PhabricatorDifferentialManagementWorkflow',
'PhabricatorDifferentialRevisionTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorDiffusionApplication' => 'PhabricatorApplication',
'PhabricatorDiffusionBlameSetting' => 'PhabricatorInternalSetting',
'PhabricatorDiffusionConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorDisabledUserController' => 'PhabricatorAuthController',
'PhabricatorDisplayPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorDisqusAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorDividerEditField' => 'PhabricatorEditField',
'PhabricatorDividerProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorDivinerApplication' => 'PhabricatorApplication',
'PhabricatorDocumentEngine' => 'Phobject',
'PhabricatorDocumentRef' => 'Phobject',
'PhabricatorDocumentRenderingEngine' => 'Phobject',
'PhabricatorDoorkeeperApplication' => 'PhabricatorApplication',
'PhabricatorDoubleExportField' => 'PhabricatorExportField',
'PhabricatorDraft' => 'PhabricatorDraftDAO',
'PhabricatorDraftDAO' => 'PhabricatorLiskDAO',
'PhabricatorDraftEngine' => 'Phobject',
'PhabricatorDrydockApplication' => 'PhabricatorApplication',
'PhabricatorDuoAuthFactor' => 'PhabricatorAuthFactor',
'PhabricatorDuoFuture' => 'FutureProxy',
'PhabricatorEdgeChangeRecord' => 'Phobject',
'PhabricatorEdgeChangeRecordTestCase' => 'PhabricatorTestCase',
'PhabricatorEdgeConfig' => 'PhabricatorEdgeConstants',
'PhabricatorEdgeConstants' => 'Phobject',
'PhabricatorEdgeCycleException' => 'Exception',
'PhabricatorEdgeEditType' => 'PhabricatorPHIDListEditType',
'PhabricatorEdgeEditor' => 'Phobject',
'PhabricatorEdgeGraph' => 'AbstractDirectedGraph',
'PhabricatorEdgeObject' => array(
'Phobject',
'PhabricatorPolicyInterface',
),
'PhabricatorEdgeObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorEdgeQuery' => 'PhabricatorQuery',
'PhabricatorEdgeTestCase' => 'PhabricatorTestCase',
'PhabricatorEdgeType' => 'Phobject',
'PhabricatorEdgeTypeTestCase' => 'PhabricatorTestCase',
'PhabricatorEdgesDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorEditEngine' => array(
'Phobject',
'PhabricatorPolicyInterface',
),
'PhabricatorEditEngineAPIMethod' => 'ConduitAPIMethod',
'PhabricatorEditEngineBulkJobType' => 'PhabricatorWorkerBulkJobType',
'PhabricatorEditEngineCheckboxesCommentAction' => 'PhabricatorEditEngineCommentAction',
'PhabricatorEditEngineColumnsCommentAction' => 'PhabricatorEditEngineCommentAction',
'PhabricatorEditEngineCommentAction' => 'Phobject',
'PhabricatorEditEngineCommentActionGroup' => 'Phobject',
'PhabricatorEditEngineConfiguration' => array(
'PhabricatorSearchDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'PhabricatorEditEngineConfigurationDefaultCreateController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationDefaultsController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationDisableController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationEditController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationEditEngine' => 'PhabricatorEditEngine',
'PhabricatorEditEngineConfigurationEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorEditEngineConfigurationIsEditController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationListController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationLockController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationPHIDType' => 'PhabricatorPHIDType',
'PhabricatorEditEngineConfigurationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorEditEngineConfigurationReorderController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationSaveController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorEditEngineConfigurationSortController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationSubtypeController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorEditEngineConfigurationTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorEditEngineConfigurationViewController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineController' => 'PhabricatorApplicationTransactionController',
'PhabricatorEditEngineDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorEditEngineDefaultLock' => 'PhabricatorEditEngineLock',
'PhabricatorEditEngineExtension' => 'Phobject',
'PhabricatorEditEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorEditEngineListController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineLock' => 'Phobject',
'PhabricatorEditEngineMFAEngine' => 'Phobject',
'PhabricatorEditEnginePointsCommentAction' => 'PhabricatorEditEngineCommentAction',
'PhabricatorEditEngineProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorEditEngineQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorEditEngineSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorEditEngineSelectCommentAction' => 'PhabricatorEditEngineCommentAction',
'PhabricatorEditEngineSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorEditEngineStaticCommentAction' => 'PhabricatorEditEngineCommentAction',
'PhabricatorEditEngineSubtype' => 'Phobject',
'PhabricatorEditEngineSubtypeMap' => 'Phobject',
'PhabricatorEditEngineSubtypeTestCase' => 'PhabricatorTestCase',
'PhabricatorEditEngineTokenizerCommentAction' => 'PhabricatorEditEngineCommentAction',
'PhabricatorEditField' => 'Phobject',
'PhabricatorEditPage' => 'Phobject',
'PhabricatorEditType' => 'Phobject',
'PhabricatorEditor' => 'Phobject',
'PhabricatorEditorExtension' => 'Phobject',
'PhabricatorEditorExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorEditorMailEngineExtension' => 'PhabricatorMailEngineExtension',
'PhabricatorEditorMultipleSetting' => 'PhabricatorSelectSetting',
'PhabricatorEditorSetting' => 'PhabricatorStringSetting',
'PhabricatorElasticFulltextStorageEngine' => 'PhabricatorFulltextStorageEngine',
'PhabricatorElasticsearchHost' => 'PhabricatorSearchHost',
'PhabricatorElasticsearchSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorEmailAddressesSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorEmailContentSource' => 'PhabricatorContentSource',
'PhabricatorEmailDeliverySettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorEmailFormatSetting' => 'PhabricatorSelectSetting',
'PhabricatorEmailFormatSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorEmailLoginController' => 'PhabricatorAuthController',
'PhabricatorEmailNotificationsSetting' => 'PhabricatorSelectSetting',
'PhabricatorEmailPreferencesSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorEmailRePrefixSetting' => 'PhabricatorSelectSetting',
'PhabricatorEmailSelfActionsSetting' => 'PhabricatorSelectSetting',
'PhabricatorEmailStampsSetting' => 'PhabricatorSelectSetting',
'PhabricatorEmailTagsSetting' => 'PhabricatorInternalSetting',
'PhabricatorEmailVarySubjectsSetting' => 'PhabricatorSelectSetting',
'PhabricatorEmailVerificationController' => 'PhabricatorAuthController',
'PhabricatorEmbedFileRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorEmojiDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorEmojiRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorEmojiTranslation' => 'PhutilTranslation',
'PhabricatorEmptyQueryException' => 'Exception',
'PhabricatorEnumConfigType' => 'PhabricatorTextConfigType',
'PhabricatorEnv' => 'Phobject',
'PhabricatorEnvTestCase' => 'PhabricatorTestCase',
'PhabricatorEpochEditField' => 'PhabricatorEditField',
'PhabricatorEpochExportField' => 'PhabricatorExportField',
'PhabricatorEvent' => 'PhutilEvent',
'PhabricatorEventEngine' => 'Phobject',
'PhabricatorEventListener' => 'PhutilEventListener',
'PhabricatorEventType' => 'PhutilEventType',
'PhabricatorExampleEventListener' => 'PhabricatorEventListener',
'PhabricatorExcelExportFormat' => 'PhabricatorExportFormat',
'PhabricatorExecFutureFileUploadSource' => 'PhabricatorFileUploadSource',
'PhabricatorExportEngine' => 'Phobject',
'PhabricatorExportEngineBulkJobType' => 'PhabricatorWorkerSingleBulkJobType',
'PhabricatorExportEngineExtension' => 'Phobject',
'PhabricatorExportField' => 'Phobject',
'PhabricatorExportFormat' => 'Phobject',
'PhabricatorExportFormatSetting' => 'PhabricatorInternalSetting',
'PhabricatorExtendingPhabricatorConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorExtensionsSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorExternalAccount' => array(
'PhabricatorUserDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorExternalAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorExternalAccountsSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorExtraConfigSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorFacebookAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorFact' => 'Phobject',
'PhabricatorFactAggregate' => 'PhabricatorFactDAO',
'PhabricatorFactApplication' => 'PhabricatorApplication',
'PhabricatorFactChartController' => 'PhabricatorFactController',
'PhabricatorFactController' => 'PhabricatorController',
'PhabricatorFactCursor' => 'PhabricatorFactDAO',
'PhabricatorFactDAO' => 'PhabricatorLiskDAO',
'PhabricatorFactDaemon' => 'PhabricatorDaemon',
'PhabricatorFactDatapointQuery' => 'Phobject',
'PhabricatorFactDimension' => 'PhabricatorFactDAO',
'PhabricatorFactEngine' => 'Phobject',
'PhabricatorFactEngineTestCase' => 'PhabricatorTestCase',
'PhabricatorFactHomeController' => 'PhabricatorFactController',
'PhabricatorFactIntDatapoint' => 'PhabricatorFactDAO',
'PhabricatorFactKeyDimension' => 'PhabricatorFactDimension',
'PhabricatorFactManagementAnalyzeWorkflow' => 'PhabricatorFactManagementWorkflow',
'PhabricatorFactManagementCursorsWorkflow' => 'PhabricatorFactManagementWorkflow',
'PhabricatorFactManagementDestroyWorkflow' => 'PhabricatorFactManagementWorkflow',
'PhabricatorFactManagementListWorkflow' => 'PhabricatorFactManagementWorkflow',
'PhabricatorFactManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorFactObjectController' => 'PhabricatorFactController',
'PhabricatorFactObjectDimension' => 'PhabricatorFactDimension',
'PhabricatorFactRaw' => 'PhabricatorFactDAO',
'PhabricatorFactUpdateIterator' => 'PhutilBufferedIterator',
'PhabricatorFaviconRef' => 'Phobject',
'PhabricatorFaviconRefQuery' => 'Phobject',
'PhabricatorFavoritesApplication' => 'PhabricatorApplication',
'PhabricatorFavoritesController' => 'PhabricatorController',
'PhabricatorFavoritesMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension',
'PhabricatorFavoritesMenuItemController' => 'PhabricatorFavoritesController',
'PhabricatorFavoritesProfileMenuEngine' => 'PhabricatorProfileMenuEngine',
'PhabricatorFaxContentSource' => 'PhabricatorContentSource',
'PhabricatorFeedApplication' => 'PhabricatorApplication',
'PhabricatorFeedBuilder' => 'Phobject',
'PhabricatorFeedConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorFeedController' => 'PhabricatorController',
'PhabricatorFeedDAO' => 'PhabricatorLiskDAO',
'PhabricatorFeedDetailController' => 'PhabricatorFeedController',
'PhabricatorFeedListController' => 'PhabricatorFeedController',
'PhabricatorFeedManagementRepublishWorkflow' => 'PhabricatorFeedManagementWorkflow',
'PhabricatorFeedManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorFeedQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorFeedSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorFeedStory' => array(
'Phobject',
'PhabricatorPolicyInterface',
'PhabricatorMarkupInterface',
),
'PhabricatorFeedStoryData' => array(
'PhabricatorFeedDAO',
'PhabricatorDestructibleInterface',
),
'PhabricatorFeedStoryNotification' => 'PhabricatorFeedDAO',
'PhabricatorFeedStoryPublisher' => 'Phobject',
'PhabricatorFeedStoryReference' => 'PhabricatorFeedDAO',
'PhabricatorFerretEngine' => 'Phobject',
'PhabricatorFerretEngineTestCase' => 'PhabricatorTestCase',
'PhabricatorFerretFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension',
'PhabricatorFerretFulltextStorageEngine' => 'PhabricatorFulltextStorageEngine',
'PhabricatorFerretMetadata' => 'Phobject',
'PhabricatorFerretSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorFile' => array(
'PhabricatorFileDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorSubscribableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
'PhabricatorIndexableInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorFileAES256StorageFormat' => 'PhabricatorFileStorageFormat',
'PhabricatorFileBundleLoader' => 'Phobject',
'PhabricatorFileChunk' => array(
'PhabricatorFileDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorFileChunkIterator' => array(
'Phobject',
'Iterator',
),
'PhabricatorFileChunkQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorFileComposeController' => 'PhabricatorFileController',
'PhabricatorFileController' => 'PhabricatorController',
'PhabricatorFileDAO' => 'PhabricatorLiskDAO',
'PhabricatorFileDataController' => 'PhabricatorFileController',
'PhabricatorFileDeleteController' => 'PhabricatorFileController',
'PhabricatorFileDeleteTransaction' => 'PhabricatorFileTransactionType',
'PhabricatorFileDocumentController' => 'PhabricatorFileController',
'PhabricatorFileDocumentRenderingEngine' => 'PhabricatorDocumentRenderingEngine',
'PhabricatorFileDropUploadController' => 'PhabricatorFileController',
'PhabricatorFileEditController' => 'PhabricatorFileController',
'PhabricatorFileEditEngine' => 'PhabricatorEditEngine',
'PhabricatorFileEditField' => 'PhabricatorEditField',
'PhabricatorFileEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorFileExternalRequest' => array(
'PhabricatorFileDAO',
'PhabricatorDestructibleInterface',
),
'PhabricatorFileExternalRequestGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorFileFilePHIDType' => 'PhabricatorPHIDType',
'PhabricatorFileHasObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorFileIconSetSelectController' => 'PhabricatorFileController',
'PhabricatorFileImageMacro' => array(
'PhabricatorFileDAO',
'PhabricatorSubscribableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorFlaggableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorPolicyInterface',
),
'PhabricatorFileImageProxyController' => 'PhabricatorFileController',
'PhabricatorFileImageTransform' => 'PhabricatorFileTransform',
'PhabricatorFileIntegrityException' => 'Exception',
'PhabricatorFileLightboxController' => 'PhabricatorFileController',
'PhabricatorFileLinkView' => 'AphrontTagView',
'PhabricatorFileListController' => 'PhabricatorFileController',
'PhabricatorFileNameNgrams' => 'PhabricatorSearchNgrams',
'PhabricatorFileNameTransaction' => 'PhabricatorFileTransactionType',
'PhabricatorFileQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorFileROT13StorageFormat' => 'PhabricatorFileStorageFormat',
'PhabricatorFileRawStorageFormat' => 'PhabricatorFileStorageFormat',
'PhabricatorFileSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorFileSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorFileSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO',
'PhabricatorFileStorageConfigurationException' => 'Exception',
'PhabricatorFileStorageEngine' => 'Phobject',
'PhabricatorFileStorageEngineTestCase' => 'PhabricatorTestCase',
'PhabricatorFileStorageFormat' => 'Phobject',
'PhabricatorFileStorageFormatTestCase' => 'PhabricatorTestCase',
'PhabricatorFileTemporaryGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorFileTestCase' => 'PhabricatorTestCase',
'PhabricatorFileTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorFileThumbnailTransform' => 'PhabricatorFileImageTransform',
'PhabricatorFileTransaction' => 'PhabricatorModularTransaction',
'PhabricatorFileTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorFileTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorFileTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorFileTransform' => 'Phobject',
'PhabricatorFileTransformController' => 'PhabricatorFileController',
'PhabricatorFileTransformListController' => 'PhabricatorFileController',
'PhabricatorFileTransformTestCase' => 'PhabricatorTestCase',
'PhabricatorFileUploadController' => 'PhabricatorFileController',
'PhabricatorFileUploadDialogController' => 'PhabricatorFileController',
'PhabricatorFileUploadException' => 'Exception',
'PhabricatorFileUploadSource' => 'Phobject',
'PhabricatorFileUploadSourceByteLimitException' => 'Exception',
'PhabricatorFileViewController' => 'PhabricatorFileController',
'PhabricatorFileinfoSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorFilesApplication' => 'PhabricatorApplication',
'PhabricatorFilesApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel',
'PhabricatorFilesBuiltinFile' => 'Phobject',
'PhabricatorFilesComposeAvatarBuiltinFile' => 'PhabricatorFilesBuiltinFile',
'PhabricatorFilesComposeIconBuiltinFile' => 'PhabricatorFilesBuiltinFile',
'PhabricatorFilesConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorFilesManagementCatWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementCompactWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementCycleWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementEncodeWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementEnginesWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementGenerateKeyWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementIntegrityWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementMigrateWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementRebuildWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorFilesOnDiskBuiltinFile' => 'PhabricatorFilesBuiltinFile',
'PhabricatorFilesOutboundRequestAction' => 'PhabricatorSystemAction',
'PhabricatorFiletreeVisibleSetting' => 'PhabricatorInternalSetting',
'PhabricatorFiletreeWidthSetting' => 'PhabricatorInternalSetting',
'PhabricatorFlag' => array(
'PhabricatorFlagDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorFlagAddFlagHeraldAction' => 'HeraldAction',
'PhabricatorFlagColor' => 'PhabricatorFlagConstants',
'PhabricatorFlagConstants' => 'Phobject',
'PhabricatorFlagController' => 'PhabricatorController',
'PhabricatorFlagDAO' => 'PhabricatorLiskDAO',
'PhabricatorFlagDeleteController' => 'PhabricatorFlagController',
'PhabricatorFlagDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorFlagEditController' => 'PhabricatorFlagController',
'PhabricatorFlagListController' => 'PhabricatorFlagController',
'PhabricatorFlagQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorFlagSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorFlagSelectControl' => 'AphrontFormControl',
'PhabricatorFlaggableInterface' => 'PhabricatorPHIDInterface',
'PhabricatorFlagsApplication' => 'PhabricatorApplication',
'PhabricatorFlagsUIEventListener' => 'PhabricatorEventListener',
'PhabricatorFulltextEngine' => 'Phobject',
'PhabricatorFulltextEngineExtension' => 'Phobject',
'PhabricatorFulltextEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorFulltextIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
'PhabricatorFulltextInterface' => 'PhabricatorIndexableInterface',
'PhabricatorFulltextResultSet' => 'Phobject',
'PhabricatorFulltextStorageEngine' => 'Phobject',
'PhabricatorFulltextToken' => 'Phobject',
'PhabricatorFundApplication' => 'PhabricatorApplication',
'PhabricatorGDSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorGarbageCollector' => 'Phobject',
'PhabricatorGarbageCollectorManagementCollectWorkflow' => 'PhabricatorGarbageCollectorManagementWorkflow',
'PhabricatorGarbageCollectorManagementCompactEdgesWorkflow' => 'PhabricatorGarbageCollectorManagementWorkflow',
'PhabricatorGarbageCollectorManagementSetPolicyWorkflow' => 'PhabricatorGarbageCollectorManagementWorkflow',
'PhabricatorGarbageCollectorManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorGeneralCachePurger' => 'PhabricatorCachePurger',
'PhabricatorGestureUIExample' => 'PhabricatorUIExample',
'PhabricatorGitGraphStream' => 'PhabricatorRepositoryGraphStream',
'PhabricatorGitHubAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorGlobalLock' => 'PhutilLock',
'PhabricatorGlobalUploadTargetView' => 'AphrontView',
'PhabricatorGoogleAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorGuidanceContext' => 'Phobject',
'PhabricatorGuidanceEngine' => 'Phobject',
'PhabricatorGuidanceEngineExtension' => 'Phobject',
'PhabricatorGuidanceMessage' => 'Phobject',
'PhabricatorGuideApplication' => 'PhabricatorApplication',
'PhabricatorGuideController' => 'PhabricatorController',
'PhabricatorGuideInstallModule' => 'PhabricatorGuideModule',
'PhabricatorGuideItemView' => 'Phobject',
'PhabricatorGuideListView' => 'AphrontView',
'PhabricatorGuideModule' => 'Phobject',
'PhabricatorGuideModuleController' => 'PhabricatorGuideController',
'PhabricatorGuideQuickStartModule' => 'PhabricatorGuideModule',
'PhabricatorHMACTestCase' => 'PhabricatorTestCase',
'PhabricatorHTTPParameterTypeTableView' => 'AphrontView',
'PhabricatorHandleList' => array(
'Phobject',
'Iterator',
'ArrayAccess',
'Countable',
),
'PhabricatorHandleObjectSelectorDataView' => 'Phobject',
'PhabricatorHandlePool' => 'Phobject',
'PhabricatorHandlePoolTestCase' => 'PhabricatorTestCase',
'PhabricatorHandleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorHandleRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorHandlesEditField' => 'PhabricatorPHIDListEditField',
'PhabricatorHarbormasterApplication' => 'PhabricatorApplication',
'PhabricatorHash' => 'Phobject',
'PhabricatorHashTestCase' => 'PhabricatorTestCase',
'PhabricatorHelpApplication' => 'PhabricatorApplication',
'PhabricatorHelpController' => 'PhabricatorController',
'PhabricatorHelpDocumentationController' => 'PhabricatorHelpController',
'PhabricatorHelpEditorProtocolController' => 'PhabricatorHelpController',
'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController',
'PhabricatorHeraldApplication' => 'PhabricatorApplication',
'PhabricatorHeraldContentSource' => 'PhabricatorContentSource',
'PhabricatorHexdumpDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorHighSecurityRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorHomeApplication' => 'PhabricatorApplication',
'PhabricatorHomeConstants' => 'PhabricatorHomeController',
'PhabricatorHomeController' => 'PhabricatorController',
'PhabricatorHomeLauncherProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorHomeMenuItemController' => 'PhabricatorHomeController',
'PhabricatorHomeProfileMenuEngine' => 'PhabricatorProfileMenuEngine',
'PhabricatorHomeProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorHovercardEngineExtension' => 'Phobject',
'PhabricatorHovercardEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorIDExportField' => 'PhabricatorExportField',
'PhabricatorIDsSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorIDsSearchField' => 'PhabricatorSearchField',
'PhabricatorIconDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorIconRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorIconSet' => 'Phobject',
'PhabricatorIconSetEditField' => 'PhabricatorEditField',
'PhabricatorIconSetIcon' => 'Phobject',
'PhabricatorImageDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorImageMacroRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorImageRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorImageTransformer' => 'Phobject',
'PhabricatorImagemagickSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorInFlightErrorView' => 'AphrontView',
'PhabricatorIndexEngine' => 'Phobject',
'PhabricatorIndexEngineExtension' => 'Phobject',
'PhabricatorIndexEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorInfrastructureTestCase' => 'PhabricatorTestCase',
'PhabricatorInlineCommentController' => 'PhabricatorController',
'PhabricatorInlineCommentInterface' => 'PhabricatorMarkupInterface',
'PhabricatorInlineCommentPreviewController' => 'PhabricatorController',
'PhabricatorInlineSummaryView' => 'AphrontView',
'PhabricatorInstructionsEditField' => 'PhabricatorEditField',
'PhabricatorIntConfigType' => 'PhabricatorTextConfigType',
'PhabricatorIntEditField' => 'PhabricatorEditField',
'PhabricatorIntExportField' => 'PhabricatorExportField',
'PhabricatorInternalSetting' => 'PhabricatorSetting',
'PhabricatorInternationalizationManagementExtractWorkflow' => 'PhabricatorInternationalizationManagementWorkflow',
'PhabricatorInternationalizationManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorInvalidConfigSetupCheck' => 'PhabricatorSetupCheck',
+ 'PhabricatorInvalidQueryCursorException' => 'Exception',
'PhabricatorIteratedMD5PasswordHasher' => 'PhabricatorPasswordHasher',
'PhabricatorIteratedMD5PasswordHasherTestCase' => 'PhabricatorTestCase',
'PhabricatorIteratorFileUploadSource' => 'PhabricatorFileUploadSource',
'PhabricatorJIRAAuthProvider' => 'PhabricatorOAuth1AuthProvider',
'PhabricatorJSONConfigType' => 'PhabricatorTextConfigType',
'PhabricatorJSONDocumentEngine' => 'PhabricatorTextDocumentEngine',
'PhabricatorJSONExportFormat' => 'PhabricatorExportFormat',
'PhabricatorJavelinLinter' => 'ArcanistLinter',
'PhabricatorJiraIssueHasObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorJupyterDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorKeyValueDatabaseCache' => 'PhutilKeyValueCache',
'PhabricatorKeyValueSerializingCacheProxy' => 'PhutilKeyValueCacheProxy',
'PhabricatorKeyboardRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorKeyring' => 'Phobject',
'PhabricatorKeyringConfigOptionType' => 'PhabricatorConfigJSONOptionType',
'PhabricatorLDAPAuthProvider' => 'PhabricatorAuthProvider',
'PhabricatorLabelProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorLanguageSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorLegalpadApplication' => 'PhabricatorApplication',
'PhabricatorLegalpadDocumentPHIDType' => 'PhabricatorPHIDType',
'PhabricatorLegalpadSignaturePolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorLibraryTestCase' => 'PhutilLibraryTestCase',
'PhabricatorLinkProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorLipsumArtist' => 'Phobject',
'PhabricatorLipsumContentSource' => 'PhabricatorContentSource',
'PhabricatorLipsumGenerateWorkflow' => 'PhabricatorLipsumManagementWorkflow',
'PhabricatorLipsumManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorLipsumMondrianArtist' => 'PhabricatorLipsumArtist',
'PhabricatorLiskDAO' => 'LiskDAO',
'PhabricatorLiskExportEngineExtension' => 'PhabricatorExportEngineExtension',
'PhabricatorLiskFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension',
'PhabricatorLiskSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorLiskSerializer' => 'Phobject',
'PhabricatorListExportField' => 'PhabricatorExportField',
'PhabricatorLocalDiskFileStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorLocalTimeTestCase' => 'PhabricatorTestCase',
'PhabricatorLocaleScopeGuard' => 'Phobject',
'PhabricatorLocaleScopeGuardTestCase' => 'PhabricatorTestCase',
'PhabricatorLockLogManagementWorkflow' => 'PhabricatorLockManagementWorkflow',
'PhabricatorLockManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorLogTriggerAction' => 'PhabricatorTriggerAction',
'PhabricatorLogoutController' => 'PhabricatorAuthController',
'PhabricatorLunarPhasePolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorMacroApplication' => 'PhabricatorApplication',
'PhabricatorMacroAudioBehaviorTransaction' => 'PhabricatorMacroTransactionType',
'PhabricatorMacroAudioController' => 'PhabricatorMacroController',
'PhabricatorMacroAudioTransaction' => 'PhabricatorMacroTransactionType',
'PhabricatorMacroController' => 'PhabricatorController',
'PhabricatorMacroDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorMacroDisableController' => 'PhabricatorMacroController',
'PhabricatorMacroDisabledTransaction' => 'PhabricatorMacroTransactionType',
'PhabricatorMacroEditController' => 'PhameBlogController',
'PhabricatorMacroEditEngine' => 'PhabricatorEditEngine',
'PhabricatorMacroEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorMacroFileTransaction' => 'PhabricatorMacroTransactionType',
'PhabricatorMacroListController' => 'PhabricatorMacroController',
'PhabricatorMacroMacroPHIDType' => 'PhabricatorPHIDType',
'PhabricatorMacroMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorMacroManageCapability' => 'PhabricatorPolicyCapability',
'PhabricatorMacroMemeController' => 'PhabricatorMacroController',
'PhabricatorMacroMemeDialogController' => 'PhabricatorMacroController',
'PhabricatorMacroNameTransaction' => 'PhabricatorMacroTransactionType',
'PhabricatorMacroQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorMacroReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorMacroSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorMacroTestCase' => 'PhabricatorTestCase',
'PhabricatorMacroTransaction' => 'PhabricatorModularTransaction',
'PhabricatorMacroTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorMacroTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorMacroTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorMacroViewController' => 'PhabricatorMacroController',
'PhabricatorMailAdapter' => 'Phobject',
+ 'PhabricatorMailAdapterTestCase' => 'PhabricatorTestCase',
'PhabricatorMailAmazonSESAdapter' => 'PhabricatorMailAdapter',
'PhabricatorMailAmazonSNSAdapter' => 'PhabricatorMailAdapter',
'PhabricatorMailAttachment' => 'Phobject',
'PhabricatorMailConfigTestCase' => 'PhabricatorTestCase',
'PhabricatorMailEmailEngine' => 'PhabricatorMailMessageEngine',
'PhabricatorMailEmailHeraldField' => 'HeraldField',
'PhabricatorMailEmailHeraldFieldGroup' => 'HeraldFieldGroup',
'PhabricatorMailEmailMessage' => 'PhabricatorMailExternalMessage',
'PhabricatorMailEmailSubjectHeraldField' => 'PhabricatorMailEmailHeraldField',
'PhabricatorMailEngineExtension' => 'Phobject',
'PhabricatorMailExternalMessage' => 'Phobject',
'PhabricatorMailHeader' => 'Phobject',
'PhabricatorMailMailgunAdapter' => 'PhabricatorMailAdapter',
'PhabricatorMailManagementListInboundWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementListOutboundWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementReceiveTestWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementResendWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementSendTestWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementShowInboundWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementShowOutboundWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementUnverifyWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementVolumeWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorMailMessageEngine' => 'Phobject',
'PhabricatorMailMustEncryptHeraldAction' => 'HeraldAction',
'PhabricatorMailOutboundMailHeraldAdapter' => 'HeraldAdapter',
'PhabricatorMailOutboundRoutingHeraldAction' => 'HeraldAction',
'PhabricatorMailOutboundRoutingSelfEmailHeraldAction' => 'PhabricatorMailOutboundRoutingHeraldAction',
'PhabricatorMailOutboundRoutingSelfNotificationHeraldAction' => 'PhabricatorMailOutboundRoutingHeraldAction',
'PhabricatorMailOutboundStatus' => 'Phobject',
'PhabricatorMailPostmarkAdapter' => 'PhabricatorMailAdapter',
'PhabricatorMailPropertiesDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorMailReceiver' => 'Phobject',
'PhabricatorMailReceiverTestCase' => 'PhabricatorTestCase',
'PhabricatorMailReplyHandler' => 'Phobject',
'PhabricatorMailRoutingRule' => 'Phobject',
'PhabricatorMailSMSEngine' => 'PhabricatorMailMessageEngine',
'PhabricatorMailSMSMessage' => 'PhabricatorMailExternalMessage',
'PhabricatorMailSMTPAdapter' => 'PhabricatorMailAdapter',
'PhabricatorMailSendGridAdapter' => 'PhabricatorMailAdapter',
'PhabricatorMailSendmailAdapter' => 'PhabricatorMailAdapter',
'PhabricatorMailSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorMailStamp' => 'Phobject',
'PhabricatorMailTarget' => 'Phobject',
'PhabricatorMailTestAdapter' => 'PhabricatorMailAdapter',
'PhabricatorMailTwilioAdapter' => 'PhabricatorMailAdapter',
'PhabricatorMailUtil' => 'Phobject',
'PhabricatorMainMenuBarExtension' => 'Phobject',
'PhabricatorMainMenuSearchView' => 'AphrontView',
'PhabricatorMainMenuView' => 'AphrontView',
'PhabricatorManageProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorManagementWorkflow' => 'PhutilArgumentWorkflow',
'PhabricatorManiphestApplication' => 'PhabricatorApplication',
'PhabricatorManiphestConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorManiphestTaskFactEngine' => 'PhabricatorTransactionFactEngine',
'PhabricatorManiphestTaskTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorManualActivitySetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorMarkupCache' => 'PhabricatorCacheDAO',
'PhabricatorMarkupEngine' => 'Phobject',
'PhabricatorMarkupEngineTestCase' => 'PhabricatorTestCase',
'PhabricatorMarkupOneOff' => array(
'Phobject',
'PhabricatorMarkupInterface',
),
'PhabricatorMarkupPreviewController' => 'PhabricatorController',
'PhabricatorMemeEngine' => 'Phobject',
'PhabricatorMemeRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorMentionRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorMercurialGraphStream' => 'PhabricatorRepositoryGraphStream',
'PhabricatorMetaMTAActor' => 'Phobject',
'PhabricatorMetaMTAActorQuery' => 'PhabricatorQuery',
'PhabricatorMetaMTAApplication' => 'PhabricatorApplication',
'PhabricatorMetaMTAApplicationEmail' => array(
'PhabricatorMetaMTADAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSpacesInterface',
),
'PhabricatorMetaMTAApplicationEmailDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorMetaMTAApplicationEmailEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorMetaMTAApplicationEmailHeraldField' => 'HeraldField',
'PhabricatorMetaMTAApplicationEmailPHIDType' => 'PhabricatorPHIDType',
'PhabricatorMetaMTAApplicationEmailPanel' => 'PhabricatorApplicationConfigurationPanel',
'PhabricatorMetaMTAApplicationEmailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorMetaMTAApplicationEmailTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorMetaMTAApplicationEmailTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorMetaMTAConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorMetaMTAController' => 'PhabricatorController',
'PhabricatorMetaMTADAO' => 'PhabricatorLiskDAO',
'PhabricatorMetaMTAEmailBodyParser' => 'Phobject',
'PhabricatorMetaMTAEmailBodyParserTestCase' => 'PhabricatorTestCase',
'PhabricatorMetaMTAEmailHeraldAction' => 'HeraldAction',
'PhabricatorMetaMTAEmailOthersHeraldAction' => 'PhabricatorMetaMTAEmailHeraldAction',
'PhabricatorMetaMTAEmailSelfHeraldAction' => 'PhabricatorMetaMTAEmailHeraldAction',
'PhabricatorMetaMTAErrorMailAction' => 'PhabricatorSystemAction',
'PhabricatorMetaMTAMail' => array(
'PhabricatorMetaMTADAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorMetaMTAMailBody' => 'Phobject',
'PhabricatorMetaMTAMailBodyTestCase' => 'PhabricatorTestCase',
'PhabricatorMetaMTAMailHasRecipientEdgeType' => 'PhabricatorEdgeType',
'PhabricatorMetaMTAMailListController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAMailPHIDType' => 'PhabricatorPHIDType',
'PhabricatorMetaMTAMailProperties' => array(
'PhabricatorMetaMTADAO',
'PhabricatorPolicyInterface',
),
'PhabricatorMetaMTAMailPropertiesQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorMetaMTAMailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorMetaMTAMailSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorMetaMTAMailSection' => 'Phobject',
'PhabricatorMetaMTAMailTestCase' => 'PhabricatorTestCase',
'PhabricatorMetaMTAMailViewController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAMailableDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorMetaMTAMailableFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorMetaMTAMailgunReceiveController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAMemberQuery' => 'PhabricatorQuery',
'PhabricatorMetaMTAPermanentFailureException' => 'Exception',
'PhabricatorMetaMTAPostmarkReceiveController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAReceivedMail' => 'PhabricatorMetaMTADAO',
'PhabricatorMetaMTAReceivedMailProcessingException' => 'Exception',
'PhabricatorMetaMTAReceivedMailTestCase' => 'PhabricatorTestCase',
'PhabricatorMetaMTASchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorMetaMTASendGridReceiveController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAWorker' => 'PhabricatorWorker',
+ 'PhabricatorMetronome' => 'Phobject',
+ 'PhabricatorMetronomeTestCase' => 'PhabricatorTestCase',
'PhabricatorMetronomicTriggerClock' => 'PhabricatorTriggerClock',
'PhabricatorModularTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorModularTransactionType' => 'Phobject',
'PhabricatorMonogramDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension',
'PhabricatorMonospacedFontSetting' => 'PhabricatorStringSetting',
'PhabricatorMonospacedTextareasSetting' => 'PhabricatorSelectSetting',
'PhabricatorMotivatorProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorMultiColumnUIExample' => 'PhabricatorUIExample',
'PhabricatorMultiFactorSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorMultimeterApplication' => 'PhabricatorApplication',
'PhabricatorMustVerifyEmailController' => 'PhabricatorAuthController',
'PhabricatorMutedByEdgeType' => 'PhabricatorEdgeType',
'PhabricatorMutedEdgeType' => 'PhabricatorEdgeType',
'PhabricatorMySQLConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorMySQLSearchHost' => 'PhabricatorSearchHost',
'PhabricatorMySQLSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorNamedQuery' => array(
'PhabricatorSearchDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorNamedQueryConfig' => array(
'PhabricatorSearchDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorNamedQueryConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorNamedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorNavigationRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorNeverTriggerClock' => 'PhabricatorTriggerClock',
'PhabricatorNgramsIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
'PhabricatorNgramsInterface' => 'PhabricatorIndexableInterface',
'PhabricatorNotificationBuilder' => 'Phobject',
'PhabricatorNotificationClearController' => 'PhabricatorNotificationController',
'PhabricatorNotificationClient' => 'Phobject',
'PhabricatorNotificationConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorNotificationController' => 'PhabricatorController',
'PhabricatorNotificationDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorNotificationIndividualController' => 'PhabricatorNotificationController',
'PhabricatorNotificationListController' => 'PhabricatorNotificationController',
'PhabricatorNotificationPanelController' => 'PhabricatorNotificationController',
'PhabricatorNotificationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorNotificationSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorNotificationServerRef' => 'Phobject',
'PhabricatorNotificationServersConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorNotificationStatusView' => 'AphrontTagView',
'PhabricatorNotificationTestController' => 'PhabricatorNotificationController',
'PhabricatorNotificationUIExample' => 'PhabricatorUIExample',
'PhabricatorNotificationsApplication' => 'PhabricatorApplication',
'PhabricatorNotificationsSetting' => 'PhabricatorInternalSetting',
'PhabricatorNotificationsSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorNuanceApplication' => 'PhabricatorApplication',
'PhabricatorOAuth1AuthProvider' => 'PhabricatorOAuthAuthProvider',
'PhabricatorOAuth1SecretTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
'PhabricatorOAuth2AuthProvider' => 'PhabricatorOAuthAuthProvider',
'PhabricatorOAuthAuthProvider' => 'PhabricatorAuthProvider',
'PhabricatorOAuthClientAuthorization' => array(
'PhabricatorOAuthServerDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorOAuthClientAuthorizationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorOAuthClientController' => 'PhabricatorOAuthServerController',
'PhabricatorOAuthClientDisableController' => 'PhabricatorOAuthClientController',
'PhabricatorOAuthClientEditController' => 'PhabricatorOAuthClientController',
'PhabricatorOAuthClientListController' => 'PhabricatorOAuthClientController',
'PhabricatorOAuthClientSecretController' => 'PhabricatorOAuthClientController',
'PhabricatorOAuthClientTestController' => 'PhabricatorOAuthClientController',
'PhabricatorOAuthClientViewController' => 'PhabricatorOAuthClientController',
'PhabricatorOAuthResponse' => 'AphrontResponse',
'PhabricatorOAuthServer' => 'Phobject',
'PhabricatorOAuthServerAccessToken' => 'PhabricatorOAuthServerDAO',
'PhabricatorOAuthServerApplication' => 'PhabricatorApplication',
'PhabricatorOAuthServerAuthController' => 'PhabricatorOAuthServerController',
'PhabricatorOAuthServerAuthorizationCode' => 'PhabricatorOAuthServerDAO',
'PhabricatorOAuthServerAuthorizationsSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorOAuthServerClient' => array(
'PhabricatorOAuthServerDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorOAuthServerClientAuthorizationPHIDType' => 'PhabricatorPHIDType',
'PhabricatorOAuthServerClientPHIDType' => 'PhabricatorPHIDType',
'PhabricatorOAuthServerClientQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorOAuthServerClientSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorOAuthServerController' => 'PhabricatorController',
'PhabricatorOAuthServerCreateClientsCapability' => 'PhabricatorPolicyCapability',
'PhabricatorOAuthServerDAO' => 'PhabricatorLiskDAO',
'PhabricatorOAuthServerEditEngine' => 'PhabricatorEditEngine',
'PhabricatorOAuthServerEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorOAuthServerSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorOAuthServerScope' => 'Phobject',
'PhabricatorOAuthServerTestCase' => 'PhabricatorTestCase',
'PhabricatorOAuthServerTokenController' => 'PhabricatorOAuthServerController',
'PhabricatorOAuthServerTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorOAuthServerTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorObjectGraph' => 'AbstractDirectedGraph',
'PhabricatorObjectHandle' => array(
'Phobject',
'PhabricatorPolicyInterface',
),
'PhabricatorObjectHasAsanaSubtaskEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasAsanaTaskEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasContributorEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasDraftEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasFileEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasJiraIssueEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasSubscriberEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasUnsubscriberEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasWatcherEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectListQuery' => 'Phobject',
'PhabricatorObjectListQueryTestCase' => 'PhabricatorTestCase',
'PhabricatorObjectMailReceiver' => 'PhabricatorMailReceiver',
'PhabricatorObjectMailReceiverTestCase' => 'PhabricatorTestCase',
'PhabricatorObjectMentionedByObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectMentionsObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorObjectRelationship' => 'Phobject',
'PhabricatorObjectRelationshipList' => 'Phobject',
'PhabricatorObjectRelationshipSource' => 'Phobject',
'PhabricatorObjectRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorObjectSelectorDialog' => 'Phobject',
'PhabricatorObjectStatus' => 'Phobject',
'PhabricatorOffsetPagedQuery' => 'PhabricatorQuery',
'PhabricatorOldWorldContentSource' => 'PhabricatorContentSource',
'PhabricatorOlderInlinesSetting' => 'PhabricatorSelectSetting',
'PhabricatorOneTimeTriggerClock' => 'PhabricatorTriggerClock',
'PhabricatorOpcodeCacheSpec' => 'PhabricatorCacheSpec',
+ 'PhabricatorOptionExportField' => 'PhabricatorExportField',
'PhabricatorOptionGroupSetting' => 'PhabricatorSetting',
'PhabricatorOwnerPathQuery' => 'Phobject',
'PhabricatorOwnersApplication' => 'PhabricatorApplication',
'PhabricatorOwnersArchiveController' => 'PhabricatorOwnersController',
+ 'PhabricatorOwnersAuditRule' => 'Phobject',
'PhabricatorOwnersConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorOwnersConfiguredCustomField' => array(
'PhabricatorOwnersCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'PhabricatorOwnersController' => 'PhabricatorController',
'PhabricatorOwnersCustomField' => 'PhabricatorCustomField',
'PhabricatorOwnersCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
'PhabricatorOwnersCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
'PhabricatorOwnersCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
'PhabricatorOwnersDAO' => 'PhabricatorLiskDAO',
'PhabricatorOwnersDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PhabricatorOwnersDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PhabricatorOwnersDetailController' => 'PhabricatorOwnersController',
'PhabricatorOwnersEditController' => 'PhabricatorOwnersController',
'PhabricatorOwnersHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension',
'PhabricatorOwnersListController' => 'PhabricatorOwnersController',
'PhabricatorOwnersOwner' => 'PhabricatorOwnersDAO',
'PhabricatorOwnersPackage' => array(
'PhabricatorOwnersDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorOwnersPackageAuditingTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageAutoreviewTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhabricatorOwnersPackageDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorOwnersPackageDescriptionTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageDominionTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageEditEngine' => 'PhabricatorEditEngine',
'PhabricatorOwnersPackageFerretEngine' => 'PhabricatorFerretEngine',
'PhabricatorOwnersPackageFulltextEngine' => 'PhabricatorFulltextEngine',
'PhabricatorOwnersPackageFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorOwnersPackageIgnoredTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageNameNgrams' => 'PhabricatorSearchNgrams',
'PhabricatorOwnersPackageNameTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageOwnerDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorOwnersPackageOwnersTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackagePHIDType' => 'PhabricatorPHIDType',
'PhabricatorOwnersPackagePathsTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackagePrimaryTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorOwnersPackageRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorOwnersPackageSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorOwnersPackageStatusTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageTestCase' => 'PhabricatorTestCase',
'PhabricatorOwnersPackageTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorOwnersPackageTransaction' => 'PhabricatorModularTransaction',
'PhabricatorOwnersPackageTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorOwnersPackageTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorOwnersPackageTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorOwnersPath' => 'PhabricatorOwnersDAO',
'PhabricatorOwnersPathContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhabricatorOwnersPathsController' => 'PhabricatorOwnersController',
'PhabricatorOwnersPathsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorOwnersSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorOwnersSearchField' => 'PhabricatorSearchTokenizerField',
'PhabricatorPDFDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorPHDConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPHID' => 'Phobject',
'PhabricatorPHIDConstants' => 'Phobject',
'PhabricatorPHIDExportField' => 'PhabricatorExportField',
'PhabricatorPHIDListEditField' => 'PhabricatorEditField',
'PhabricatorPHIDListEditType' => 'PhabricatorEditType',
'PhabricatorPHIDListExportField' => 'PhabricatorListExportField',
'PhabricatorPHIDMailStamp' => 'PhabricatorMailStamp',
'PhabricatorPHIDResolver' => 'Phobject',
'PhabricatorPHIDType' => 'Phobject',
'PhabricatorPHIDTypeTestCase' => 'PhutilTestCase',
'PhabricatorPHIDsSearchField' => 'PhabricatorSearchField',
'PhabricatorPHPASTApplication' => 'PhabricatorApplication',
'PhabricatorPHPConfigSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorPHPPreflightSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorPackagesApplication' => 'PhabricatorApplication',
'PhabricatorPackagesController' => 'PhabricatorController',
'PhabricatorPackagesCreatePublisherCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPackagesDAO' => 'PhabricatorLiskDAO',
'PhabricatorPackagesEditEngine' => 'PhabricatorEditEngine',
'PhabricatorPackagesEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorPackagesNgrams' => 'PhabricatorSearchNgrams',
'PhabricatorPackagesPackage' => array(
'PhabricatorPackagesDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSubscribableInterface',
'PhabricatorProjectInterface',
'PhabricatorConduitResultInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorPackagesPackageController' => 'PhabricatorPackagesController',
'PhabricatorPackagesPackageDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorPackagesPackageDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPackagesPackageDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPackagesPackageEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhabricatorPackagesPackageEditController' => 'PhabricatorPackagesPackageController',
'PhabricatorPackagesPackageEditEngine' => 'PhabricatorPackagesEditEngine',
'PhabricatorPackagesPackageEditor' => 'PhabricatorPackagesEditor',
'PhabricatorPackagesPackageKeyTransaction' => 'PhabricatorPackagesPackageTransactionType',
'PhabricatorPackagesPackageListController' => 'PhabricatorPackagesPackageController',
'PhabricatorPackagesPackageListView' => 'PhabricatorPackagesView',
'PhabricatorPackagesPackageNameNgrams' => 'PhabricatorPackagesNgrams',
'PhabricatorPackagesPackageNameTransaction' => 'PhabricatorPackagesPackageTransactionType',
'PhabricatorPackagesPackagePHIDType' => 'PhabricatorPHIDType',
'PhabricatorPackagesPackagePublisherTransaction' => 'PhabricatorPackagesPackageTransactionType',
'PhabricatorPackagesPackageQuery' => 'PhabricatorPackagesQuery',
'PhabricatorPackagesPackageSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorPackagesPackageSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorPackagesPackageTransaction' => 'PhabricatorModularTransaction',
'PhabricatorPackagesPackageTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorPackagesPackageTransactionType' => 'PhabricatorPackagesTransactionType',
'PhabricatorPackagesPackageViewController' => 'PhabricatorPackagesPackageController',
'PhabricatorPackagesPublisher' => array(
'PhabricatorPackagesDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSubscribableInterface',
'PhabricatorProjectInterface',
'PhabricatorConduitResultInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorPackagesPublisherController' => 'PhabricatorPackagesController',
'PhabricatorPackagesPublisherDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorPackagesPublisherDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPackagesPublisherEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhabricatorPackagesPublisherEditController' => 'PhabricatorPackagesPublisherController',
'PhabricatorPackagesPublisherEditEngine' => 'PhabricatorPackagesEditEngine',
'PhabricatorPackagesPublisherEditor' => 'PhabricatorPackagesEditor',
'PhabricatorPackagesPublisherKeyTransaction' => 'PhabricatorPackagesPublisherTransactionType',
'PhabricatorPackagesPublisherListController' => 'PhabricatorPackagesPublisherController',
'PhabricatorPackagesPublisherListView' => 'PhabricatorPackagesView',
'PhabricatorPackagesPublisherNameNgrams' => 'PhabricatorPackagesNgrams',
'PhabricatorPackagesPublisherNameTransaction' => 'PhabricatorPackagesPublisherTransactionType',
'PhabricatorPackagesPublisherPHIDType' => 'PhabricatorPHIDType',
'PhabricatorPackagesPublisherQuery' => 'PhabricatorPackagesQuery',
'PhabricatorPackagesPublisherSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorPackagesPublisherSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorPackagesPublisherTransaction' => 'PhabricatorModularTransaction',
'PhabricatorPackagesPublisherTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorPackagesPublisherTransactionType' => 'PhabricatorPackagesTransactionType',
'PhabricatorPackagesPublisherViewController' => 'PhabricatorPackagesPublisherController',
'PhabricatorPackagesQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorPackagesSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorPackagesTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorPackagesVersion' => array(
'PhabricatorPackagesDAO',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSubscribableInterface',
'PhabricatorProjectInterface',
'PhabricatorConduitResultInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorPackagesVersionController' => 'PhabricatorPackagesController',
'PhabricatorPackagesVersionEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhabricatorPackagesVersionEditController' => 'PhabricatorPackagesVersionController',
'PhabricatorPackagesVersionEditEngine' => 'PhabricatorPackagesEditEngine',
'PhabricatorPackagesVersionEditor' => 'PhabricatorPackagesEditor',
'PhabricatorPackagesVersionListController' => 'PhabricatorPackagesVersionController',
'PhabricatorPackagesVersionListView' => 'PhabricatorPackagesView',
'PhabricatorPackagesVersionNameNgrams' => 'PhabricatorPackagesNgrams',
'PhabricatorPackagesVersionNameTransaction' => 'PhabricatorPackagesVersionTransactionType',
'PhabricatorPackagesVersionPHIDType' => 'PhabricatorPHIDType',
'PhabricatorPackagesVersionPackageTransaction' => 'PhabricatorPackagesVersionTransactionType',
'PhabricatorPackagesVersionQuery' => 'PhabricatorPackagesQuery',
'PhabricatorPackagesVersionSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorPackagesVersionSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorPackagesVersionTransaction' => 'PhabricatorModularTransaction',
'PhabricatorPackagesVersionTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorPackagesVersionTransactionType' => 'PhabricatorPackagesTransactionType',
'PhabricatorPackagesVersionViewController' => 'PhabricatorPackagesVersionController',
'PhabricatorPackagesView' => 'AphrontView',
'PhabricatorPagerUIExample' => 'PhabricatorUIExample',
'PhabricatorPassphraseApplication' => 'PhabricatorApplication',
'PhabricatorPasswordAuthProvider' => 'PhabricatorAuthProvider',
'PhabricatorPasswordDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorPasswordHasher' => 'Phobject',
'PhabricatorPasswordHasherTestCase' => 'PhabricatorTestCase',
'PhabricatorPasswordHasherUnavailableException' => 'Exception',
'PhabricatorPasswordSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorPaste' => array(
'PhabricatorPasteDAO',
'PhabricatorSubscribableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorFlaggableInterface',
'PhabricatorMentionableInterface',
'PhabricatorPolicyInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorSpacesInterface',
'PhabricatorConduitResultInterface',
),
'PhabricatorPasteApplication' => 'PhabricatorApplication',
'PhabricatorPasteArchiveController' => 'PhabricatorPasteController',
'PhabricatorPasteContentSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorPasteContentTransaction' => 'PhabricatorPasteTransactionType',
'PhabricatorPasteController' => 'PhabricatorController',
'PhabricatorPasteDAO' => 'PhabricatorLiskDAO',
'PhabricatorPasteEditController' => 'PhabricatorPasteController',
'PhabricatorPasteEditEngine' => 'PhabricatorEditEngine',
'PhabricatorPasteEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorPasteFilenameContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhabricatorPasteLanguageTransaction' => 'PhabricatorPasteTransactionType',
'PhabricatorPasteListController' => 'PhabricatorPasteController',
'PhabricatorPastePastePHIDType' => 'PhabricatorPHIDType',
'PhabricatorPasteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorPasteRawController' => 'PhabricatorPasteController',
'PhabricatorPasteRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorPasteSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorPasteSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorPasteSnippet' => 'Phobject',
'PhabricatorPasteStatusTransaction' => 'PhabricatorPasteTransactionType',
'PhabricatorPasteTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorPasteTitleTransaction' => 'PhabricatorPasteTransactionType',
'PhabricatorPasteTransaction' => 'PhabricatorModularTransaction',
'PhabricatorPasteTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorPasteTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorPasteTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorPasteViewController' => 'PhabricatorPasteController',
'PhabricatorPathSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorPeopleAnyOwnerDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorPeopleApplication' => 'PhabricatorApplication',
'PhabricatorPeopleApproveController' => 'PhabricatorPeopleController',
'PhabricatorPeopleAvailabilitySearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorPeopleBadgesProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorPeopleCommitsProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorPeopleController' => 'PhabricatorController',
'PhabricatorPeopleCreateController' => 'PhabricatorPeopleController',
'PhabricatorPeopleCreateGuidanceContext' => 'PhabricatorGuidanceContext',
'PhabricatorPeopleDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorPeopleDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension',
'PhabricatorPeopleDeleteController' => 'PhabricatorPeopleController',
'PhabricatorPeopleDetailsProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorPeopleDisableController' => 'PhabricatorPeopleController',
'PhabricatorPeopleEmpowerController' => 'PhabricatorPeopleController',
'PhabricatorPeopleExternalPHIDType' => 'PhabricatorPHIDType',
'PhabricatorPeopleIconSet' => 'PhabricatorIconSet',
'PhabricatorPeopleInviteController' => 'PhabricatorPeopleController',
'PhabricatorPeopleInviteListController' => 'PhabricatorPeopleInviteController',
'PhabricatorPeopleInviteSendController' => 'PhabricatorPeopleInviteController',
- 'PhabricatorPeopleLdapController' => 'PhabricatorPeopleController',
'PhabricatorPeopleListController' => 'PhabricatorPeopleController',
'PhabricatorPeopleLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorPeopleLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorPeopleLogsController' => 'PhabricatorPeopleController',
'PhabricatorPeopleMailEngine' => 'Phobject',
'PhabricatorPeopleMailEngineException' => 'Exception',
'PhabricatorPeopleManageProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorPeopleManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorPeopleNewController' => 'PhabricatorPeopleController',
'PhabricatorPeopleNoOwnerDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorPeopleOwnerDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorPeoplePictureProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorPeopleProfileBadgesController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleProfileCommitsController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleProfileController' => 'PhabricatorPeopleController',
'PhabricatorPeopleProfileEditController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleProfileImageWorkflow' => 'PhabricatorPeopleManagementWorkflow',
'PhabricatorPeopleProfileManageController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleProfileMenuEngine' => 'PhabricatorProfileMenuEngine',
'PhabricatorPeopleProfilePictureController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleProfileRevisionsController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleProfileTasksController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleProfileViewController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorPeopleRenameController' => 'PhabricatorPeopleController',
'PhabricatorPeopleRevisionsProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorPeopleSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorPeopleTasksProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorPeopleTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorPeopleTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorPeopleUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorPeopleUserPHIDType' => 'PhabricatorPHIDType',
+ 'PhabricatorPeopleUsernameMailEngine' => 'PhabricatorPeopleMailEngine',
'PhabricatorPeopleWelcomeController' => 'PhabricatorPeopleController',
'PhabricatorPeopleWelcomeMailEngine' => 'PhabricatorPeopleMailEngine',
'PhabricatorPhabricatorAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorPhameApplication' => 'PhabricatorApplication',
'PhabricatorPhameBlogPHIDType' => 'PhabricatorPHIDType',
'PhabricatorPhamePostPHIDType' => 'PhabricatorPHIDType',
'PhabricatorPhluxApplication' => 'PhabricatorApplication',
'PhabricatorPholioApplication' => 'PhabricatorApplication',
'PhabricatorPholioMockTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorPhoneNumber' => 'Phobject',
'PhabricatorPhoneNumberTestCase' => 'PhabricatorTestCase',
'PhabricatorPhortuneApplication' => 'PhabricatorApplication',
'PhabricatorPhortuneContentSource' => 'PhabricatorContentSource',
'PhabricatorPhortuneManagementInvoiceWorkflow' => 'PhabricatorPhortuneManagementWorkflow',
'PhabricatorPhortuneManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorPhortuneTestCase' => 'PhabricatorTestCase',
'PhabricatorPhragmentApplication' => 'PhabricatorApplication',
'PhabricatorPhrequentApplication' => 'PhabricatorApplication',
'PhabricatorPhrictionApplication' => 'PhabricatorApplication',
'PhabricatorPhurlApplication' => 'PhabricatorApplication',
'PhabricatorPhurlConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPhurlController' => 'PhabricatorController',
'PhabricatorPhurlDAO' => 'PhabricatorLiskDAO',
'PhabricatorPhurlLinkRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorPhurlRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorPhurlSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorPhurlShortURLController' => 'PhabricatorPhurlController',
'PhabricatorPhurlShortURLDefaultController' => 'PhabricatorPhurlController',
'PhabricatorPhurlURL' => array(
'PhabricatorPhurlDAO',
'PhabricatorPolicyInterface',
'PhabricatorProjectInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorSubscribableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorDestructibleInterface',
'PhabricatorMentionableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorSpacesInterface',
'PhabricatorConduitResultInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorPhurlURLAccessController' => 'PhabricatorPhurlController',
'PhabricatorPhurlURLAliasTransaction' => 'PhabricatorPhurlURLTransactionType',
'PhabricatorPhurlURLCreateCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPhurlURLDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorPhurlURLDescriptionTransaction' => 'PhabricatorPhurlURLTransactionType',
'PhabricatorPhurlURLEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhabricatorPhurlURLEditController' => 'PhabricatorPhurlController',
'PhabricatorPhurlURLEditEngine' => 'PhabricatorEditEngine',
'PhabricatorPhurlURLEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorPhurlURLListController' => 'PhabricatorPhurlController',
'PhabricatorPhurlURLLongURLTransaction' => 'PhabricatorPhurlURLTransactionType',
'PhabricatorPhurlURLMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorPhurlURLNameNgrams' => 'PhabricatorSearchNgrams',
'PhabricatorPhurlURLNameTransaction' => 'PhabricatorPhurlURLTransactionType',
'PhabricatorPhurlURLPHIDType' => 'PhabricatorPHIDType',
'PhabricatorPhurlURLQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorPhurlURLReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorPhurlURLSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorPhurlURLSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorPhurlURLTransaction' => 'PhabricatorModularTransaction',
'PhabricatorPhurlURLTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorPhurlURLTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorPhurlURLTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorPhurlURLViewController' => 'PhabricatorPhurlController',
'PhabricatorPinnedApplicationsSetting' => 'PhabricatorInternalSetting',
'PhabricatorPirateEnglishTranslation' => 'PhutilTranslation',
'PhabricatorPlatformSite' => 'PhabricatorSite',
'PhabricatorPointsEditField' => 'PhabricatorEditField',
'PhabricatorPointsFact' => 'PhabricatorFact',
'PhabricatorPolicies' => 'PhabricatorPolicyConstants',
'PhabricatorPolicy' => array(
'PhabricatorPolicyDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorPolicyApplication' => 'PhabricatorApplication',
'PhabricatorPolicyAwareQuery' => 'PhabricatorOffsetPagedQuery',
'PhabricatorPolicyAwareTestQuery' => 'PhabricatorPolicyAwareQuery',
'PhabricatorPolicyCanEditCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPolicyCanInteractCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPolicyCanJoinCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPolicyCanViewCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPolicyCapability' => 'Phobject',
'PhabricatorPolicyCapabilityTestCase' => 'PhabricatorTestCase',
'PhabricatorPolicyCodex' => 'Phobject',
'PhabricatorPolicyCodexRuleDescription' => 'Phobject',
'PhabricatorPolicyConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPolicyConstants' => 'Phobject',
'PhabricatorPolicyController' => 'PhabricatorController',
'PhabricatorPolicyDAO' => 'PhabricatorLiskDAO',
'PhabricatorPolicyDataTestCase' => 'PhabricatorTestCase',
'PhabricatorPolicyEditController' => 'PhabricatorPolicyController',
'PhabricatorPolicyEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorPolicyEditField' => 'PhabricatorEditField',
'PhabricatorPolicyException' => 'Exception',
'PhabricatorPolicyExplainController' => 'PhabricatorPolicyController',
'PhabricatorPolicyFavoritesSetting' => 'PhabricatorInternalSetting',
'PhabricatorPolicyFilter' => 'Phobject',
'PhabricatorPolicyInterface' => 'PhabricatorPHIDInterface',
'PhabricatorPolicyManagementShowWorkflow' => 'PhabricatorPolicyManagementWorkflow',
'PhabricatorPolicyManagementUnlockWorkflow' => 'PhabricatorPolicyManagementWorkflow',
'PhabricatorPolicyManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorPolicyPHIDTypePolicy' => 'PhabricatorPHIDType',
'PhabricatorPolicyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorPolicyRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorPolicyRule' => 'Phobject',
'PhabricatorPolicySearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorPolicyStrengthConstants' => 'PhabricatorPolicyConstants',
'PhabricatorPolicyTestCase' => 'PhabricatorTestCase',
'PhabricatorPolicyTestObject' => array(
'Phobject',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
),
'PhabricatorPolicyType' => 'PhabricatorPolicyConstants',
'PhabricatorPonderApplication' => 'PhabricatorApplication',
'PhabricatorProfileMenuEditEngine' => 'PhabricatorEditEngine',
'PhabricatorProfileMenuEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorProfileMenuEngine' => 'Phobject',
'PhabricatorProfileMenuItem' => 'Phobject',
'PhabricatorProfileMenuItemConfiguration' => array(
'PhabricatorSearchDAO',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhabricatorProfileMenuItemConfigurationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorProfileMenuItemConfigurationTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorProfileMenuItemConfigurationTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorProfileMenuItemIconSet' => 'PhabricatorIconSet',
'PhabricatorProfileMenuItemPHIDType' => 'PhabricatorPHIDType',
'PhabricatorProject' => array(
'PhabricatorProjectDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorFlaggableInterface',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorDestructibleInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'PhabricatorConduitResultInterface',
'PhabricatorColumnProxyInterface',
'PhabricatorSpacesInterface',
'PhabricatorEditEngineSubtypeInterface',
),
'PhabricatorProjectAddHeraldAction' => 'PhabricatorProjectHeraldAction',
'PhabricatorProjectApplication' => 'PhabricatorApplication',
'PhabricatorProjectArchiveController' => 'PhabricatorProjectController',
'PhabricatorProjectBoardBackgroundController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectBoardController' => 'PhabricatorProjectController',
'PhabricatorProjectBoardDisableController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectBoardImportController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectBoardManageController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectBoardReorderController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectBoardViewController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectBuiltinsExample' => 'PhabricatorUIExample',
'PhabricatorProjectCardView' => 'AphrontTagView',
'PhabricatorProjectColorTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectColorsConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorProjectColumn' => array(
'PhabricatorProjectDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorConduitResultInterface',
),
+ 'PhabricatorProjectColumnAuthorOrder' => 'PhabricatorProjectColumnOrder',
+ 'PhabricatorProjectColumnCreatedOrder' => 'PhabricatorProjectColumnOrder',
'PhabricatorProjectColumnDetailController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectColumnEditController' => 'PhabricatorProjectBoardController',
+ 'PhabricatorProjectColumnHeader' => 'Phobject',
'PhabricatorProjectColumnHideController' => 'PhabricatorProjectBoardController',
+ 'PhabricatorProjectColumnLimitTransaction' => 'PhabricatorProjectColumnTransactionType',
+ 'PhabricatorProjectColumnNameTransaction' => 'PhabricatorProjectColumnTransactionType',
+ 'PhabricatorProjectColumnNaturalOrder' => 'PhabricatorProjectColumnOrder',
+ 'PhabricatorProjectColumnOrder' => 'Phobject',
+ 'PhabricatorProjectColumnOwnerOrder' => 'PhabricatorProjectColumnOrder',
'PhabricatorProjectColumnPHIDType' => 'PhabricatorPHIDType',
+ 'PhabricatorProjectColumnPointsOrder' => 'PhabricatorProjectColumnOrder',
'PhabricatorProjectColumnPosition' => array(
'PhabricatorProjectDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorProjectColumnPositionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+ 'PhabricatorProjectColumnPriorityOrder' => 'PhabricatorProjectColumnOrder',
'PhabricatorProjectColumnQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+ 'PhabricatorProjectColumnRemoveTriggerController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectColumnSearchEngine' => 'PhabricatorApplicationSearchEngine',
- 'PhabricatorProjectColumnTransaction' => 'PhabricatorApplicationTransaction',
+ 'PhabricatorProjectColumnStatusOrder' => 'PhabricatorProjectColumnOrder',
+ 'PhabricatorProjectColumnStatusTransaction' => 'PhabricatorProjectColumnTransactionType',
+ 'PhabricatorProjectColumnTitleOrder' => 'PhabricatorProjectColumnOrder',
+ 'PhabricatorProjectColumnTransaction' => 'PhabricatorModularTransaction',
'PhabricatorProjectColumnTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorProjectColumnTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+ 'PhabricatorProjectColumnTransactionType' => 'PhabricatorModularTransactionType',
+ 'PhabricatorProjectColumnTriggerTransaction' => 'PhabricatorProjectColumnTransactionType',
'PhabricatorProjectConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorProjectConfiguredCustomField' => array(
'PhabricatorProjectStandardCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'PhabricatorProjectController' => 'PhabricatorController',
'PhabricatorProjectCoreTestCase' => 'PhabricatorTestCase',
'PhabricatorProjectCoverController' => 'PhabricatorProjectController',
'PhabricatorProjectCustomField' => 'PhabricatorCustomField',
'PhabricatorProjectCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
'PhabricatorProjectCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
'PhabricatorProjectCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
'PhabricatorProjectDAO' => 'PhabricatorLiskDAO',
'PhabricatorProjectDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorProjectDefaultController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectDescriptionField' => 'PhabricatorProjectStandardCustomField',
'PhabricatorProjectDetailsProfileMenuItem' => 'PhabricatorProfileMenuItem',
+ 'PhabricatorProjectDropEffect' => 'Phobject',
'PhabricatorProjectEditController' => 'PhabricatorProjectController',
'PhabricatorProjectEditEngine' => 'PhabricatorEditEngine',
'PhabricatorProjectEditPictureController' => 'PhabricatorProjectController',
'PhabricatorProjectFerretEngine' => 'PhabricatorFerretEngine',
'PhabricatorProjectFilterTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectFulltextEngine' => 'PhabricatorFulltextEngine',
'PhabricatorProjectHeraldAction' => 'HeraldAction',
'PhabricatorProjectHeraldAdapter' => 'HeraldAdapter',
'PhabricatorProjectHeraldFieldGroup' => 'HeraldFieldGroup',
'PhabricatorProjectHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension',
'PhabricatorProjectIconSet' => 'PhabricatorIconSet',
'PhabricatorProjectIconTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectIconsConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorProjectImageTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectListController' => 'PhabricatorProjectController',
'PhabricatorProjectListView' => 'AphrontView',
'PhabricatorProjectLockController' => 'PhabricatorProjectController',
'PhabricatorProjectLockTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectLogicalAncestorDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectLogicalDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectLogicalOnlyDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorProjectLogicalOrNotDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectLogicalUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectLogicalViewerDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorProjectManageController' => 'PhabricatorProjectController',
'PhabricatorProjectManageProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectMaterializedMemberEdgeType' => 'PhabricatorEdgeType',
'PhabricatorProjectMemberListView' => 'PhabricatorProjectUserListView',
'PhabricatorProjectMemberOfProjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorProjectMembersAddController' => 'PhabricatorProjectController',
'PhabricatorProjectMembersDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectMembersPolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorProjectMembersProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectMembersRemoveController' => 'PhabricatorProjectController',
'PhabricatorProjectMembersViewController' => 'PhabricatorProjectController',
'PhabricatorProjectMenuItemController' => 'PhabricatorProjectController',
'PhabricatorProjectMilestoneTransaction' => 'PhabricatorProjectTypeTransaction',
'PhabricatorProjectMoveController' => 'PhabricatorProjectController',
'PhabricatorProjectNameContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhabricatorProjectNameTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectNoProjectsDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorProjectObjectHasProjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorProjectOrUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectOrUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectPHIDResolver' => 'PhabricatorPHIDResolver',
'PhabricatorProjectParentTransaction' => 'PhabricatorProjectTypeTransaction',
'PhabricatorProjectPictureProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectPointsProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectProfileController' => 'PhabricatorProjectController',
'PhabricatorProjectProfileMenuEngine' => 'PhabricatorProfileMenuEngine',
'PhabricatorProjectProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectProjectHasMemberEdgeType' => 'PhabricatorEdgeType',
'PhabricatorProjectProjectHasObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorProjectProjectPHIDType' => 'PhabricatorPHIDType',
'PhabricatorProjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorProjectRemoveHeraldAction' => 'PhabricatorProjectHeraldAction',
'PhabricatorProjectSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorProjectSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorProjectSearchField' => 'PhabricatorSearchTokenizerField',
'PhabricatorProjectSilenceController' => 'PhabricatorProjectController',
'PhabricatorProjectSilencedEdgeType' => 'PhabricatorEdgeType',
'PhabricatorProjectSlug' => 'PhabricatorProjectDAO',
'PhabricatorProjectSlugsTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectSortTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectStandardCustomField' => array(
'PhabricatorProjectCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'PhabricatorProjectStatus' => 'Phobject',
'PhabricatorProjectStatusTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectSubprojectWarningController' => 'PhabricatorProjectController',
'PhabricatorProjectSubprojectsController' => 'PhabricatorProjectController',
'PhabricatorProjectSubprojectsProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectSubtypeDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorProjectSubtypesConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorProjectTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorProjectTransaction' => 'PhabricatorModularTransaction',
'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorProjectTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorProjectTransactionType' => 'PhabricatorModularTransactionType',
+ 'PhabricatorProjectTrigger' => array(
+ 'PhabricatorProjectDAO',
+ 'PhabricatorApplicationTransactionInterface',
+ 'PhabricatorPolicyInterface',
+ 'PhabricatorIndexableInterface',
+ 'PhabricatorDestructibleInterface',
+ ),
+ 'PhabricatorProjectTriggerController' => 'PhabricatorProjectController',
+ 'PhabricatorProjectTriggerCorruptionException' => 'Exception',
+ 'PhabricatorProjectTriggerEditController' => 'PhabricatorProjectTriggerController',
+ 'PhabricatorProjectTriggerEditor' => 'PhabricatorApplicationTransactionEditor',
+ 'PhabricatorProjectTriggerInvalidRule' => 'PhabricatorProjectTriggerRule',
+ 'PhabricatorProjectTriggerListController' => 'PhabricatorProjectTriggerController',
+ 'PhabricatorProjectTriggerManiphestPriorityRule' => 'PhabricatorProjectTriggerRule',
+ 'PhabricatorProjectTriggerManiphestStatusRule' => 'PhabricatorProjectTriggerRule',
+ 'PhabricatorProjectTriggerNameTransaction' => 'PhabricatorProjectTriggerTransactionType',
+ 'PhabricatorProjectTriggerPHIDType' => 'PhabricatorPHIDType',
+ 'PhabricatorProjectTriggerPlaySoundRule' => 'PhabricatorProjectTriggerRule',
+ 'PhabricatorProjectTriggerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+ 'PhabricatorProjectTriggerRule' => 'Phobject',
+ 'PhabricatorProjectTriggerRuleRecord' => 'Phobject',
+ 'PhabricatorProjectTriggerRulesetTransaction' => 'PhabricatorProjectTriggerTransactionType',
+ 'PhabricatorProjectTriggerSearchEngine' => 'PhabricatorApplicationSearchEngine',
+ 'PhabricatorProjectTriggerTransaction' => 'PhabricatorModularTransaction',
+ 'PhabricatorProjectTriggerTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+ 'PhabricatorProjectTriggerTransactionType' => 'PhabricatorModularTransactionType',
+ 'PhabricatorProjectTriggerUnknownRule' => 'PhabricatorProjectTriggerRule',
+ 'PhabricatorProjectTriggerUsage' => 'PhabricatorProjectDAO',
+ 'PhabricatorProjectTriggerUsageIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
+ 'PhabricatorProjectTriggerViewController' => 'PhabricatorProjectTriggerController',
'PhabricatorProjectTypeTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener',
'PhabricatorProjectUpdateController' => 'PhabricatorProjectController',
'PhabricatorProjectUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectUserListView' => 'AphrontView',
'PhabricatorProjectViewController' => 'PhabricatorProjectController',
'PhabricatorProjectWatchController' => 'PhabricatorProjectController',
'PhabricatorProjectWatcherListView' => 'PhabricatorProjectUserListView',
'PhabricatorProjectWorkboardBackgroundColor' => 'Phobject',
'PhabricatorProjectWorkboardBackgroundTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectWorkboardProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectWorkboardTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectsAllPolicyRule' => 'PhabricatorProjectsBasePolicyRule',
'PhabricatorProjectsAncestorsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorProjectsBasePolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorProjectsCurtainExtension' => 'PHUICurtainExtension',
'PhabricatorProjectsEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorProjectsEditField' => 'PhabricatorTokenizerEditField',
'PhabricatorProjectsExportEngineExtension' => 'PhabricatorExportEngineExtension',
'PhabricatorProjectsFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension',
'PhabricatorProjectsMailEngineExtension' => 'PhabricatorMailEngineExtension',
'PhabricatorProjectsMembersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorProjectsMembershipIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
'PhabricatorProjectsPolicyRule' => 'PhabricatorProjectsBasePolicyRule',
'PhabricatorProjectsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorProjectsSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorProjectsWatchersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorPronounSetting' => 'PhabricatorSelectSetting',
'PhabricatorPygmentSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorQuery' => 'Phobject',
'PhabricatorQueryConstraint' => 'Phobject',
+ 'PhabricatorQueryCursor' => 'Phobject',
'PhabricatorQueryIterator' => 'PhutilBufferedIterator',
'PhabricatorQueryOrderItem' => 'Phobject',
'PhabricatorQueryOrderTestCase' => 'PhabricatorTestCase',
'PhabricatorQueryOrderVector' => array(
'Phobject',
'Iterator',
),
'PhabricatorQuickSearchEngineExtension' => 'PhabricatorDatasourceEngineExtension',
'PhabricatorRateLimitRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorRecaptchaConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorRedirectController' => 'PhabricatorController',
'PhabricatorRefreshCSRFController' => 'PhabricatorAuthController',
'PhabricatorRegexListConfigType' => 'PhabricatorTextListConfigType',
'PhabricatorRegistrationProfile' => 'Phobject',
'PhabricatorReleephApplication' => 'PhabricatorApplication',
'PhabricatorReleephApplicationConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorRemarkupCachePurger' => 'PhabricatorCachePurger',
'PhabricatorRemarkupControl' => 'AphrontFormTextAreaControl',
'PhabricatorRemarkupCowsayBlockInterpreter' => 'PhutilRemarkupBlockInterpreter',
'PhabricatorRemarkupCustomBlockRule' => 'PhutilRemarkupBlockRule',
'PhabricatorRemarkupCustomInlineRule' => 'PhutilRemarkupRule',
'PhabricatorRemarkupDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorRemarkupEditField' => 'PhabricatorEditField',
'PhabricatorRemarkupFigletBlockInterpreter' => 'PhutilRemarkupBlockInterpreter',
'PhabricatorRemarkupUIExample' => 'PhabricatorUIExample',
'PhabricatorRepositoriesSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorRepository' => array(
'PhabricatorRepositoryDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorFlaggableInterface',
'PhabricatorMarkupInterface',
'PhabricatorDestructibleInterface',
'PhabricatorDestructibleCodexInterface',
'PhabricatorProjectInterface',
'PhabricatorSpacesInterface',
'PhabricatorConduitResultInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
),
'PhabricatorRepositoryActivateTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryAuditRequest' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositoryAutocloseOnlyTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryAutocloseTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryBlueprintsTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryBranch' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryCallsignTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryCommit' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
'PhabricatorFlaggableInterface',
'PhabricatorProjectInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorSubscribableInterface',
'PhabricatorMentionableInterface',
'HarbormasterBuildableInterface',
'HarbormasterCircleCIBuildableInterface',
'HarbormasterBuildkiteBuildableInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorTimelineInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'PhabricatorConduitResultInterface',
'PhabricatorDraftInterface',
),
'PhabricatorRepositoryCommitChangeParserWorker' => 'PhabricatorRepositoryCommitParserWorker',
'PhabricatorRepositoryCommitData' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryCommitHeraldWorker' => 'PhabricatorRepositoryCommitParserWorker',
'PhabricatorRepositoryCommitHint' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositoryCommitMessageParserWorker' => 'PhabricatorRepositoryCommitParserWorker',
'PhabricatorRepositoryCommitOwnersWorker' => 'PhabricatorRepositoryCommitParserWorker',
'PhabricatorRepositoryCommitPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositoryCommitParserWorker' => 'PhabricatorWorker',
'PhabricatorRepositoryCommitRef' => 'Phobject',
'PhabricatorRepositoryCommitTestCase' => 'PhabricatorTestCase',
'PhabricatorRepositoryConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorRepositoryCopyTimeLimitTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryDAO' => 'PhabricatorLiskDAO',
'PhabricatorRepositoryDangerousTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryDefaultBranchTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryDescriptionTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryDestructibleCodex' => 'PhabricatorDestructibleCodex',
'PhabricatorRepositoryDiscoveryEngine' => 'PhabricatorRepositoryEngine',
'PhabricatorRepositoryEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorRepositoryEncodingTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryEngine' => 'Phobject',
'PhabricatorRepositoryEnormousTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryFerretEngine' => 'PhabricatorFerretEngine',
'PhabricatorRepositoryFilesizeLimitTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryFulltextEngine' => 'PhabricatorFulltextEngine',
'PhabricatorRepositoryGitCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker',
'PhabricatorRepositoryGitCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker',
'PhabricatorRepositoryGitLFSRef' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorRepositoryGitLFSRefQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryGraphCache' => 'Phobject',
'PhabricatorRepositoryGraphStream' => 'Phobject',
'PhabricatorRepositoryIdentity' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhabricatorRepositoryIdentityAssignTransaction' => 'PhabricatorRepositoryIdentityTransactionType',
'PhabricatorRepositoryIdentityChangeWorker' => 'PhabricatorWorker',
'PhabricatorRepositoryIdentityEditEngine' => 'PhabricatorEditEngine',
'PhabricatorRepositoryIdentityFerretEngine' => 'PhabricatorFerretEngine',
'PhabricatorRepositoryIdentityPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositoryIdentityQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryIdentityTransaction' => 'PhabricatorModularTransaction',
'PhabricatorRepositoryIdentityTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorRepositoryIdentityTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorRepositoryManagementCacheWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementClusterizeWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementDiscoverWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementHintWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementImportingWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementListPathsWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementListWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementLookupUsersWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementMarkImportedWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementMarkReachableWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementMirrorWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementMovePathsWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementParentsWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementPullWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementRebuildIdentitiesWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementRefsWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementReparseWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementThawWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementUnpublishWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementUpdateWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker',
'PhabricatorRepositoryMercurialCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker',
'PhabricatorRepositoryMirror' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryMirrorEngine' => 'PhabricatorRepositoryEngine',
'PhabricatorRepositoryNameTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryNotifyTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryOldRef' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositoryParsedChange' => 'Phobject',
'PhabricatorRepositoryPullEngine' => 'PhabricatorRepositoryEngine',
'PhabricatorRepositoryPullEvent' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositoryPullEventPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositoryPullEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryPullLocalDaemon' => 'PhabricatorDaemon',
'PhabricatorRepositoryPullLocalDaemonModule' => 'PhutilDaemonOverseerModule',
'PhabricatorRepositoryPushEvent' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositoryPushEventPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositoryPushEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryPushLog' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositoryPushLogPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositoryPushLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryPushLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorRepositoryPushMailWorker' => 'PhabricatorWorker',
'PhabricatorRepositoryPushPolicyTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryPushReplyHandler' => 'PhabricatorMailReplyHandler',
'PhabricatorRepositoryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryRefCursor' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositoryRefCursorPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositoryRefCursorQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryRefEngine' => 'PhabricatorRepositoryEngine',
'PhabricatorRepositoryRefPosition' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryRepositoryPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositorySVNSubpathTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositorySchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorRepositorySearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorRepositoryServiceTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositorySlugTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryStagingURITransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryStatusMessage' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositorySvnCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker',
'PhabricatorRepositorySvnCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker',
'PhabricatorRepositorySymbol' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositorySymbolLanguagesTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositorySymbolSourcesTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositorySyncEvent' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositorySyncEventPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositorySyncEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryTestCase' => 'PhabricatorTestCase',
'PhabricatorRepositoryTouchLimitTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryTrackOnlyTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryTransaction' => 'PhabricatorModularTransaction',
'PhabricatorRepositoryTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorRepositoryTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorRepositoryType' => 'Phobject',
'PhabricatorRepositoryURI' => array(
'PhabricatorRepositoryDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorConduitResultInterface',
),
'PhabricatorRepositoryURIIndex' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryURINormalizer' => 'Phobject',
'PhabricatorRepositoryURINormalizerTestCase' => 'PhabricatorTestCase',
'PhabricatorRepositoryURIPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositoryURIQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryURITestCase' => 'PhabricatorTestCase',
'PhabricatorRepositoryURITransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorRepositoryURITransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorRepositoryVCSTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryWorkingCopyVersion' => 'PhabricatorRepositoryDAO',
'PhabricatorRequestExceptionHandler' => 'AphrontRequestExceptionHandler',
'PhabricatorResourceSite' => 'PhabricatorSite',
'PhabricatorRobotsController' => 'PhabricatorController',
'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorSMSAuthFactor' => 'PhabricatorAuthFactor',
'PhabricatorSQLPatchList' => 'Phobject',
'PhabricatorSSHKeyGenerator' => 'Phobject',
'PhabricatorSSHKeysSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorSSHLog' => 'Phobject',
'PhabricatorSSHPassthruCommand' => 'Phobject',
'PhabricatorSSHWorkflow' => 'PhutilArgumentWorkflow',
'PhabricatorSavedQuery' => array(
'PhabricatorSearchDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorSavedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorScheduleTaskTriggerAction' => 'PhabricatorTriggerAction',
'PhabricatorScopedEnv' => 'Phobject',
'PhabricatorSearchAbstractDocument' => 'Phobject',
'PhabricatorSearchApplication' => 'PhabricatorApplication',
'PhabricatorSearchApplicationSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorSearchApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel',
'PhabricatorSearchBaseController' => 'PhabricatorController',
'PhabricatorSearchCheckboxesField' => 'PhabricatorSearchField',
'PhabricatorSearchConstraintException' => 'Exception',
'PhabricatorSearchController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchCustomFieldProxyField' => 'PhabricatorSearchField',
'PhabricatorSearchDAO' => 'PhabricatorLiskDAO',
'PhabricatorSearchDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorSearchDatasourceField' => 'PhabricatorSearchTokenizerField',
'PhabricatorSearchDateControlField' => 'PhabricatorSearchField',
'PhabricatorSearchDateField' => 'PhabricatorSearchField',
'PhabricatorSearchDefaultController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchDeleteController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchDocument' => 'PhabricatorSearchDAO',
'PhabricatorSearchDocumentField' => 'PhabricatorSearchDAO',
'PhabricatorSearchDocumentFieldType' => 'Phobject',
'PhabricatorSearchDocumentQuery' => 'PhabricatorPolicyAwareQuery',
'PhabricatorSearchDocumentRelationship' => 'PhabricatorSearchDAO',
'PhabricatorSearchDocumentTypeDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorSearchEditController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchEngineAPIMethod' => 'ConduitAPIMethod',
'PhabricatorSearchEngineAttachment' => 'Phobject',
'PhabricatorSearchEngineExtension' => 'Phobject',
'PhabricatorSearchEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorSearchFerretNgramGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorSearchField' => 'Phobject',
'PhabricatorSearchHost' => 'Phobject',
'PhabricatorSearchHovercardController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchIndexVersion' => 'PhabricatorSearchDAO',
'PhabricatorSearchIndexVersionDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorSearchManagementIndexWorkflow' => 'PhabricatorSearchManagementWorkflow',
'PhabricatorSearchManagementInitWorkflow' => 'PhabricatorSearchManagementWorkflow',
'PhabricatorSearchManagementNgramsWorkflow' => 'PhabricatorSearchManagementWorkflow',
'PhabricatorSearchManagementQueryWorkflow' => 'PhabricatorSearchManagementWorkflow',
'PhabricatorSearchManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorSearchNgrams' => 'PhabricatorSearchDAO',
'PhabricatorSearchNgramsDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorSearchOrderController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchOrderField' => 'PhabricatorSearchField',
'PhabricatorSearchRelationship' => 'Phobject',
'PhabricatorSearchRelationshipController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchRelationshipSourceController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchResultBucket' => 'Phobject',
'PhabricatorSearchResultBucketGroup' => 'Phobject',
'PhabricatorSearchResultView' => 'AphrontView',
'PhabricatorSearchSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorSearchScopeSetting' => 'PhabricatorInternalSetting',
'PhabricatorSearchSelectField' => 'PhabricatorSearchField',
'PhabricatorSearchService' => 'Phobject',
'PhabricatorSearchStringListField' => 'PhabricatorSearchField',
'PhabricatorSearchSubscribersField' => 'PhabricatorSearchTokenizerField',
'PhabricatorSearchTextField' => 'PhabricatorSearchField',
'PhabricatorSearchThreeStateField' => 'PhabricatorSearchField',
'PhabricatorSearchTokenizerField' => 'PhabricatorSearchField',
'PhabricatorSearchWorker' => 'PhabricatorWorker',
'PhabricatorSecurityConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorSecuritySetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorSelectEditField' => 'PhabricatorEditField',
'PhabricatorSelectSetting' => 'PhabricatorSetting',
'PhabricatorSessionsSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorSetConfigType' => 'PhabricatorTextConfigType',
'PhabricatorSetting' => 'Phobject',
'PhabricatorSettingsAccountPanelGroup' => 'PhabricatorSettingsPanelGroup',
'PhabricatorSettingsAddEmailAction' => 'PhabricatorSystemAction',
'PhabricatorSettingsAdjustController' => 'PhabricatorController',
'PhabricatorSettingsApplication' => 'PhabricatorApplication',
'PhabricatorSettingsApplicationsPanelGroup' => 'PhabricatorSettingsPanelGroup',
'PhabricatorSettingsAuthenticationPanelGroup' => 'PhabricatorSettingsPanelGroup',
'PhabricatorSettingsDeveloperPanelGroup' => 'PhabricatorSettingsPanelGroup',
'PhabricatorSettingsEditEngine' => 'PhabricatorEditEngine',
'PhabricatorSettingsEmailPanelGroup' => 'PhabricatorSettingsPanelGroup',
'PhabricatorSettingsIssueController' => 'PhabricatorController',
'PhabricatorSettingsListController' => 'PhabricatorController',
'PhabricatorSettingsLogsPanelGroup' => 'PhabricatorSettingsPanelGroup',
'PhabricatorSettingsMainController' => 'PhabricatorController',
'PhabricatorSettingsPanel' => 'Phobject',
'PhabricatorSettingsPanelGroup' => 'Phobject',
'PhabricatorSettingsTimezoneController' => 'PhabricatorController',
'PhabricatorSetupCheck' => 'Phobject',
'PhabricatorSetupCheckTestCase' => 'PhabricatorTestCase',
'PhabricatorSetupEngine' => 'Phobject',
'PhabricatorSetupIssue' => 'Phobject',
'PhabricatorSetupIssueUIExample' => 'PhabricatorUIExample',
'PhabricatorSetupIssueView' => 'AphrontView',
'PhabricatorShortSite' => 'PhabricatorSite',
'PhabricatorShowFiletreeSetting' => 'PhabricatorSelectSetting',
'PhabricatorSimpleEditType' => 'PhabricatorEditType',
'PhabricatorSite' => 'AphrontSite',
'PhabricatorSlackAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorSlowvoteApplication' => 'PhabricatorApplication',
'PhabricatorSlowvoteChoice' => 'PhabricatorSlowvoteDAO',
'PhabricatorSlowvoteCloseController' => 'PhabricatorSlowvoteController',
'PhabricatorSlowvoteCloseTransaction' => 'PhabricatorSlowvoteTransactionType',
'PhabricatorSlowvoteCommentController' => 'PhabricatorSlowvoteController',
'PhabricatorSlowvoteController' => 'PhabricatorController',
'PhabricatorSlowvoteDAO' => 'PhabricatorLiskDAO',
'PhabricatorSlowvoteDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PhabricatorSlowvoteDescriptionTransaction' => 'PhabricatorSlowvoteTransactionType',
'PhabricatorSlowvoteEditController' => 'PhabricatorSlowvoteController',
'PhabricatorSlowvoteEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorSlowvoteListController' => 'PhabricatorSlowvoteController',
'PhabricatorSlowvoteMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorSlowvoteOption' => 'PhabricatorSlowvoteDAO',
'PhabricatorSlowvotePoll' => array(
'PhabricatorSlowvoteDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorSubscribableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSpacesInterface',
),
'PhabricatorSlowvotePollController' => 'PhabricatorSlowvoteController',
'PhabricatorSlowvotePollPHIDType' => 'PhabricatorPHIDType',
'PhabricatorSlowvoteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorSlowvoteQuestionTransaction' => 'PhabricatorSlowvoteTransactionType',
'PhabricatorSlowvoteReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorSlowvoteResponsesTransaction' => 'PhabricatorSlowvoteTransactionType',
'PhabricatorSlowvoteSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorSlowvoteSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorSlowvoteShuffleTransaction' => 'PhabricatorSlowvoteTransactionType',
'PhabricatorSlowvoteTransaction' => 'PhabricatorModularTransaction',
'PhabricatorSlowvoteTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorSlowvoteTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorSlowvoteTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorSlowvoteVoteController' => 'PhabricatorSlowvoteController',
'PhabricatorSlug' => 'Phobject',
'PhabricatorSlugTestCase' => 'PhabricatorTestCase',
'PhabricatorSourceCodeView' => 'AphrontView',
'PhabricatorSourceDocumentEngine' => 'PhabricatorTextDocumentEngine',
'PhabricatorSpaceEditField' => 'PhabricatorEditField',
'PhabricatorSpacesApplication' => 'PhabricatorApplication',
'PhabricatorSpacesArchiveController' => 'PhabricatorSpacesController',
'PhabricatorSpacesCapabilityCreateSpaces' => 'PhabricatorPolicyCapability',
'PhabricatorSpacesCapabilityDefaultEdit' => 'PhabricatorPolicyCapability',
'PhabricatorSpacesCapabilityDefaultView' => 'PhabricatorPolicyCapability',
'PhabricatorSpacesController' => 'PhabricatorController',
'PhabricatorSpacesDAO' => 'PhabricatorLiskDAO',
'PhabricatorSpacesEditController' => 'PhabricatorSpacesController',
'PhabricatorSpacesExportEngineExtension' => 'PhabricatorExportEngineExtension',
'PhabricatorSpacesInterface' => 'PhabricatorPHIDInterface',
'PhabricatorSpacesListController' => 'PhabricatorSpacesController',
'PhabricatorSpacesMailEngineExtension' => 'PhabricatorMailEngineExtension',
'PhabricatorSpacesNamespace' => array(
'PhabricatorSpacesDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorSpacesNamespaceArchiveTransaction' => 'PhabricatorSpacesNamespaceTransactionType',
'PhabricatorSpacesNamespaceDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorSpacesNamespaceDefaultTransaction' => 'PhabricatorSpacesNamespaceTransactionType',
'PhabricatorSpacesNamespaceDescriptionTransaction' => 'PhabricatorSpacesNamespaceTransactionType',
'PhabricatorSpacesNamespaceEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorSpacesNamespaceNameTransaction' => 'PhabricatorSpacesNamespaceTransactionType',
'PhabricatorSpacesNamespacePHIDType' => 'PhabricatorPHIDType',
'PhabricatorSpacesNamespaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorSpacesNamespaceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorSpacesNamespaceTransaction' => 'PhabricatorModularTransaction',
'PhabricatorSpacesNamespaceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorSpacesNamespaceTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorSpacesNoAccessController' => 'PhabricatorSpacesController',
'PhabricatorSpacesRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorSpacesSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorSpacesSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorSpacesSearchField' => 'PhabricatorSearchTokenizerField',
'PhabricatorSpacesTestCase' => 'PhabricatorTestCase',
'PhabricatorSpacesViewController' => 'PhabricatorSpacesController',
'PhabricatorStandardCustomField' => 'PhabricatorCustomField',
'PhabricatorStandardCustomFieldBlueprints' => 'PhabricatorStandardCustomFieldTokenizer',
'PhabricatorStandardCustomFieldBool' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldCredential' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldDatasource' => 'PhabricatorStandardCustomFieldTokenizer',
'PhabricatorStandardCustomFieldDate' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldHeader' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldInt' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldLink' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldPHIDs' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldRemarkup' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldSelect' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldText' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldTokenizer' => 'PhabricatorStandardCustomFieldPHIDs',
'PhabricatorStandardCustomFieldUsers' => 'PhabricatorStandardCustomFieldTokenizer',
'PhabricatorStandardPageView' => array(
'PhabricatorBarePageView',
'AphrontResponseProducerInterface',
),
'PhabricatorStandardSelectCustomFieldDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorStandardTimelineEngine' => 'PhabricatorTimelineEngine',
'PhabricatorStaticEditField' => 'PhabricatorEditField',
'PhabricatorStatusController' => 'PhabricatorController',
'PhabricatorStatusUIExample' => 'PhabricatorUIExample',
'PhabricatorStorageFixtureScopeGuard' => 'Phobject',
'PhabricatorStorageManagementAPI' => 'Phobject',
'PhabricatorStorageManagementAdjustWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementAnalyzeWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementDatabasesWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementDestroyWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementDumpWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementOptimizeWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementPartitionWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementProbeWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementQuickstartWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementRenamespaceWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementShellWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementStatusWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementUpgradeWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorStoragePatch' => 'Phobject',
'PhabricatorStorageSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorStorageSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorStringConfigType' => 'PhabricatorTextConfigType',
'PhabricatorStringExportField' => 'PhabricatorExportField',
'PhabricatorStringListConfigType' => 'PhabricatorTextListConfigType',
'PhabricatorStringListEditField' => 'PhabricatorEditField',
'PhabricatorStringListExportField' => 'PhabricatorListExportField',
'PhabricatorStringMailStamp' => 'PhabricatorMailStamp',
'PhabricatorStringSetting' => 'PhabricatorSetting',
'PhabricatorSubmitEditField' => 'PhabricatorEditField',
'PhabricatorSubscribedToObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorSubscribersEditField' => 'PhabricatorTokenizerEditField',
'PhabricatorSubscribersQuery' => 'PhabricatorQuery',
'PhabricatorSubscriptionTriggerClock' => 'PhabricatorTriggerClock',
'PhabricatorSubscriptionsAddSelfHeraldAction' => 'PhabricatorSubscriptionsHeraldAction',
'PhabricatorSubscriptionsAddSubscribersHeraldAction' => 'PhabricatorSubscriptionsHeraldAction',
'PhabricatorSubscriptionsApplication' => 'PhabricatorApplication',
'PhabricatorSubscriptionsCurtainExtension' => 'PHUICurtainExtension',
'PhabricatorSubscriptionsEditController' => 'PhabricatorController',
'PhabricatorSubscriptionsEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor',
'PhabricatorSubscriptionsExportEngineExtension' => 'PhabricatorExportEngineExtension',
'PhabricatorSubscriptionsFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension',
'PhabricatorSubscriptionsHeraldAction' => 'HeraldAction',
'PhabricatorSubscriptionsListController' => 'PhabricatorController',
'PhabricatorSubscriptionsMailEngineExtension' => 'PhabricatorMailEngineExtension',
'PhabricatorSubscriptionsMuteController' => 'PhabricatorController',
'PhabricatorSubscriptionsRemoveSelfHeraldAction' => 'PhabricatorSubscriptionsHeraldAction',
'PhabricatorSubscriptionsRemoveSubscribersHeraldAction' => 'PhabricatorSubscriptionsHeraldAction',
'PhabricatorSubscriptionsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorSubscriptionsSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorSubscriptionsSubscribeEmailCommand' => 'MetaMTAEmailTransactionCommand',
'PhabricatorSubscriptionsSubscribersPolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorSubscriptionsTransactionController' => 'PhabricatorController',
'PhabricatorSubscriptionsUIEventListener' => 'PhabricatorEventListener',
'PhabricatorSubscriptionsUnsubscribeEmailCommand' => 'MetaMTAEmailTransactionCommand',
'PhabricatorSubtypeEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorSupportApplication' => 'PhabricatorApplication',
'PhabricatorSyntaxHighlighter' => 'Phobject',
'PhabricatorSyntaxHighlightingConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorSyntaxStyle' => 'Phobject',
'PhabricatorSystemAction' => 'Phobject',
'PhabricatorSystemActionEngine' => 'Phobject',
'PhabricatorSystemActionGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorSystemActionLog' => 'PhabricatorSystemDAO',
'PhabricatorSystemActionRateLimitException' => 'Exception',
'PhabricatorSystemApplication' => 'PhabricatorApplication',
'PhabricatorSystemDAO' => 'PhabricatorLiskDAO',
'PhabricatorSystemDestructionGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorSystemDestructionLog' => 'PhabricatorSystemDAO',
'PhabricatorSystemObjectController' => 'PhabricatorController',
'PhabricatorSystemReadOnlyController' => 'PhabricatorController',
'PhabricatorSystemRemoveDestroyWorkflow' => 'PhabricatorSystemRemoveWorkflow',
'PhabricatorSystemRemoveLogWorkflow' => 'PhabricatorSystemRemoveWorkflow',
'PhabricatorSystemRemoveWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorSystemSelectEncodingController' => 'PhabricatorController',
'PhabricatorSystemSelectHighlightController' => 'PhabricatorController',
'PhabricatorTOTPAuthFactor' => 'PhabricatorAuthFactor',
'PhabricatorTOTPAuthFactorTestCase' => 'PhabricatorTestCase',
'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon',
'PhabricatorTaskmasterDaemonModule' => 'PhutilDaemonOverseerModule',
'PhabricatorTestApplication' => 'PhabricatorApplication',
'PhabricatorTestCase' => 'PhutilTestCase',
'PhabricatorTestController' => 'PhabricatorController',
'PhabricatorTestDataGenerator' => 'Phobject',
'PhabricatorTestNoCycleEdgeType' => 'PhabricatorEdgeType',
'PhabricatorTestStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorTestWorker' => 'PhabricatorWorker',
'PhabricatorTextAreaEditField' => 'PhabricatorEditField',
'PhabricatorTextConfigType' => 'PhabricatorConfigType',
'PhabricatorTextDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorTextEditField' => 'PhabricatorEditField',
'PhabricatorTextExportFormat' => 'PhabricatorExportFormat',
'PhabricatorTextListConfigType' => 'PhabricatorTextConfigType',
'PhabricatorTime' => 'Phobject',
'PhabricatorTimeFormatSetting' => 'PhabricatorSelectSetting',
'PhabricatorTimeGuard' => 'Phobject',
'PhabricatorTimeTestCase' => 'PhabricatorTestCase',
'PhabricatorTimelineEngine' => 'Phobject',
'PhabricatorTimezoneIgnoreOffsetSetting' => 'PhabricatorInternalSetting',
'PhabricatorTimezoneSetting' => 'PhabricatorOptionGroupSetting',
'PhabricatorTimezoneSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorTitleGlyphsSetting' => 'PhabricatorSelectSetting',
'PhabricatorToken' => array(
'PhabricatorTokenDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorTokenController' => 'PhabricatorController',
'PhabricatorTokenCount' => 'PhabricatorTokenDAO',
'PhabricatorTokenCountQuery' => 'PhabricatorOffsetPagedQuery',
'PhabricatorTokenDAO' => 'PhabricatorLiskDAO',
'PhabricatorTokenDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorTokenGiveController' => 'PhabricatorTokenController',
'PhabricatorTokenGiven' => array(
'PhabricatorTokenDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorTokenGivenController' => 'PhabricatorTokenController',
'PhabricatorTokenGivenEditor' => 'PhabricatorEditor',
'PhabricatorTokenGivenFeedStory' => 'PhabricatorFeedStory',
'PhabricatorTokenGivenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorTokenLeaderController' => 'PhabricatorTokenController',
'PhabricatorTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorTokenReceiverQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorTokenTokenPHIDType' => 'PhabricatorPHIDType',
'PhabricatorTokenUIEventListener' => 'PhabricatorEventListener',
'PhabricatorTokenizerEditField' => 'PhabricatorPHIDListEditField',
'PhabricatorTokensApplication' => 'PhabricatorApplication',
'PhabricatorTokensCurtainExtension' => 'PHUICurtainExtension',
'PhabricatorTokensSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorTokensToken' => array(
'PhabricatorTokenDAO',
'PhabricatorDestructibleInterface',
'PhabricatorSubscribableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorConduitResultInterface',
),
'PhabricatorTransactionChange' => 'Phobject',
'PhabricatorTransactionFactEngine' => 'PhabricatorFactEngine',
'PhabricatorTransactionRemarkupChange' => 'PhabricatorTransactionChange',
'PhabricatorTransactions' => 'Phobject',
'PhabricatorTransactionsApplication' => 'PhabricatorApplication',
'PhabricatorTransactionsDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorTransactionsFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension',
'PhabricatorTransformedFile' => 'PhabricatorFileDAO',
'PhabricatorTranslationSetting' => 'PhabricatorOptionGroupSetting',
'PhabricatorTranslationsConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorTriggerAction' => 'Phobject',
'PhabricatorTriggerClock' => 'Phobject',
'PhabricatorTriggerClockTestCase' => 'PhabricatorTestCase',
'PhabricatorTriggerDaemon' => 'PhabricatorDaemon',
'PhabricatorTrivialTestCase' => 'PhabricatorTestCase',
'PhabricatorTwilioFuture' => 'FutureProxy',
'PhabricatorTwitchAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorTwitterAuthProvider' => 'PhabricatorOAuth1AuthProvider',
'PhabricatorTypeaheadApplication' => 'PhabricatorApplication',
'PhabricatorTypeaheadCompositeDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorTypeaheadDatasource' => 'Phobject',
'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController',
'PhabricatorTypeaheadDatasourceTestCase' => 'PhabricatorTestCase',
'PhabricatorTypeaheadFunctionHelpController' => 'PhabricatorTypeaheadDatasourceController',
'PhabricatorTypeaheadInvalidTokenException' => 'Exception',
'PhabricatorTypeaheadModularDatasourceController' => 'PhabricatorTypeaheadDatasourceController',
'PhabricatorTypeaheadMonogramDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorTypeaheadProxyDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorTypeaheadResult' => 'Phobject',
'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorTypeaheadTestNumbersDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorTypeaheadTokenView' => 'AphrontTagView',
'PhabricatorUIConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorUIExample' => 'Phobject',
'PhabricatorUIExampleRenderController' => 'PhabricatorController',
'PhabricatorUIExamplesApplication' => 'PhabricatorApplication',
'PhabricatorURIExportField' => 'PhabricatorExportField',
'PhabricatorUSEnglishTranslation' => 'PhutilTranslation',
'PhabricatorUnifiedDiffsSetting' => 'PhabricatorSelectSetting',
'PhabricatorUnitTestContentSource' => 'PhabricatorContentSource',
'PhabricatorUnitsTestCase' => 'PhabricatorTestCase',
'PhabricatorUnknownContentSource' => 'PhabricatorContentSource',
+ 'PhabricatorUnlockEngine' => 'Phobject',
'PhabricatorUnsubscribedFromObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorUser' => array(
'PhabricatorUserDAO',
'PhutilPerson',
'PhabricatorPolicyInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSSHPublicKeyInterface',
'PhabricatorFlaggableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'PhabricatorConduitResultInterface',
'PhabricatorAuthPasswordHashInterface',
),
'PhabricatorUserApproveTransaction' => 'PhabricatorUserTransactionType',
'PhabricatorUserBadgesCacheType' => 'PhabricatorUserCacheType',
'PhabricatorUserBlurbField' => 'PhabricatorUserCustomField',
'PhabricatorUserCache' => 'PhabricatorUserDAO',
'PhabricatorUserCachePurger' => 'PhabricatorCachePurger',
'PhabricatorUserCacheType' => 'Phobject',
'PhabricatorUserCardView' => 'AphrontTagView',
'PhabricatorUserConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorUserConfiguredCustomField' => array(
'PhabricatorUserCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'PhabricatorUserConfiguredCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
'PhabricatorUserCustomField' => 'PhabricatorCustomField',
'PhabricatorUserCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
'PhabricatorUserCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
'PhabricatorUserDAO' => 'PhabricatorLiskDAO',
'PhabricatorUserDisableTransaction' => 'PhabricatorUserTransactionType',
'PhabricatorUserEditEngine' => 'PhabricatorEditEngine',
'PhabricatorUserEditor' => 'PhabricatorEditor',
'PhabricatorUserEditorTestCase' => 'PhabricatorTestCase',
'PhabricatorUserEmail' => 'PhabricatorUserDAO',
'PhabricatorUserEmailTestCase' => 'PhabricatorTestCase',
'PhabricatorUserEmpowerTransaction' => 'PhabricatorUserTransactionType',
'PhabricatorUserFerretEngine' => 'PhabricatorFerretEngine',
'PhabricatorUserFulltextEngine' => 'PhabricatorFulltextEngine',
'PhabricatorUserIconField' => 'PhabricatorUserCustomField',
'PhabricatorUserLog' => array(
'PhabricatorUserDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorUserLogView' => 'AphrontView',
'PhabricatorUserMessageCountCacheType' => 'PhabricatorUserCacheType',
'PhabricatorUserNotificationCountCacheType' => 'PhabricatorUserCacheType',
'PhabricatorUserNotifyTransaction' => 'PhabricatorUserTransactionType',
'PhabricatorUserPHIDResolver' => 'PhabricatorPHIDResolver',
'PhabricatorUserPreferences' => array(
'PhabricatorUserDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhabricatorUserPreferencesCacheType' => 'PhabricatorUserCacheType',
'PhabricatorUserPreferencesEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorUserPreferencesPHIDType' => 'PhabricatorPHIDType',
'PhabricatorUserPreferencesQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorUserPreferencesSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorUserPreferencesTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorUserPreferencesTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorUserProfile' => 'PhabricatorUserDAO',
'PhabricatorUserProfileImageCacheType' => 'PhabricatorUserCacheType',
'PhabricatorUserRealNameField' => 'PhabricatorUserCustomField',
'PhabricatorUserRolesField' => 'PhabricatorUserCustomField',
'PhabricatorUserSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorUserSinceField' => 'PhabricatorUserCustomField',
'PhabricatorUserStatusField' => 'PhabricatorUserCustomField',
'PhabricatorUserTestCase' => 'PhabricatorTestCase',
'PhabricatorUserTitleField' => 'PhabricatorUserCustomField',
'PhabricatorUserTransaction' => 'PhabricatorModularTransaction',
'PhabricatorUserTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorUserTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorUserUsernameTransaction' => 'PhabricatorUserTransactionType',
'PhabricatorUsersEditField' => 'PhabricatorTokenizerEditField',
'PhabricatorUsersPolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorUsersSearchField' => 'PhabricatorSearchTokenizerField',
'PhabricatorVCSResponse' => 'AphrontResponse',
'PhabricatorVersionedDraft' => 'PhabricatorDraftDAO',
'PhabricatorVeryWowEnglishTranslation' => 'PhutilTranslation',
'PhabricatorVideoDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorViewerDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorVoidDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorWatcherHasObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorWebContentSource' => 'PhabricatorContentSource',
'PhabricatorWebServerSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorWeekStartDaySetting' => 'PhabricatorSelectSetting',
'PhabricatorWildConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorWordPressAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorWorker' => 'Phobject',
'PhabricatorWorkerActiveTask' => 'PhabricatorWorkerTask',
'PhabricatorWorkerActiveTaskQuery' => 'PhabricatorWorkerTaskQuery',
'PhabricatorWorkerArchiveTask' => 'PhabricatorWorkerTask',
'PhabricatorWorkerArchiveTaskQuery' => 'PhabricatorWorkerTaskQuery',
'PhabricatorWorkerBulkJob' => array(
'PhabricatorWorkerDAO',
'PhabricatorPolicyInterface',
'PhabricatorSubscribableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorWorkerBulkJobCreateWorker' => 'PhabricatorWorkerBulkJobWorker',
'PhabricatorWorkerBulkJobEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorWorkerBulkJobPHIDType' => 'PhabricatorPHIDType',
'PhabricatorWorkerBulkJobQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorWorkerBulkJobSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorWorkerBulkJobTaskWorker' => 'PhabricatorWorkerBulkJobWorker',
'PhabricatorWorkerBulkJobTestCase' => 'PhabricatorTestCase',
'PhabricatorWorkerBulkJobTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorWorkerBulkJobTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorWorkerBulkJobType' => 'Phobject',
'PhabricatorWorkerBulkJobWorker' => 'PhabricatorWorker',
'PhabricatorWorkerBulkTask' => 'PhabricatorWorkerDAO',
'PhabricatorWorkerDAO' => 'PhabricatorLiskDAO',
'PhabricatorWorkerDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorWorkerLeaseQuery' => 'PhabricatorQuery',
'PhabricatorWorkerManagementCancelWorkflow' => 'PhabricatorWorkerManagementWorkflow',
'PhabricatorWorkerManagementExecuteWorkflow' => 'PhabricatorWorkerManagementWorkflow',
'PhabricatorWorkerManagementFloodWorkflow' => 'PhabricatorWorkerManagementWorkflow',
'PhabricatorWorkerManagementFreeWorkflow' => 'PhabricatorWorkerManagementWorkflow',
'PhabricatorWorkerManagementRetryWorkflow' => 'PhabricatorWorkerManagementWorkflow',
'PhabricatorWorkerManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorWorkerPermanentFailureException' => 'Exception',
'PhabricatorWorkerSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorWorkerSingleBulkJobType' => 'PhabricatorWorkerBulkJobType',
'PhabricatorWorkerTask' => 'PhabricatorWorkerDAO',
'PhabricatorWorkerTaskData' => 'PhabricatorWorkerDAO',
'PhabricatorWorkerTaskDetailController' => 'PhabricatorDaemonController',
'PhabricatorWorkerTaskQuery' => 'PhabricatorQuery',
'PhabricatorWorkerTestCase' => 'PhabricatorTestCase',
'PhabricatorWorkerTrigger' => array(
'PhabricatorWorkerDAO',
'PhabricatorDestructibleInterface',
'PhabricatorPolicyInterface',
),
'PhabricatorWorkerTriggerEvent' => 'PhabricatorWorkerDAO',
'PhabricatorWorkerTriggerManagementFireWorkflow' => 'PhabricatorWorkerTriggerManagementWorkflow',
'PhabricatorWorkerTriggerManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorWorkerTriggerPHIDType' => 'PhabricatorPHIDType',
'PhabricatorWorkerTriggerQuery' => 'PhabricatorPolicyAwareQuery',
'PhabricatorWorkerYieldException' => 'Exception',
'PhabricatorWorkingCopyDiscoveryTestCase' => 'PhabricatorWorkingCopyTestCase',
'PhabricatorWorkingCopyPullTestCase' => 'PhabricatorWorkingCopyTestCase',
'PhabricatorWorkingCopyTestCase' => 'PhabricatorTestCase',
'PhabricatorXHPASTDAO' => 'PhabricatorLiskDAO',
'PhabricatorXHPASTParseTree' => 'PhabricatorXHPASTDAO',
'PhabricatorXHPASTViewController' => 'PhabricatorController',
'PhabricatorXHPASTViewFrameController' => 'PhabricatorXHPASTViewController',
'PhabricatorXHPASTViewFramesetController' => 'PhabricatorXHPASTViewController',
'PhabricatorXHPASTViewInputController' => 'PhabricatorXHPASTViewPanelController',
'PhabricatorXHPASTViewPanelController' => 'PhabricatorXHPASTViewController',
'PhabricatorXHPASTViewRunController' => 'PhabricatorXHPASTViewController',
'PhabricatorXHPASTViewStreamController' => 'PhabricatorXHPASTViewPanelController',
'PhabricatorXHPASTViewTreeController' => 'PhabricatorXHPASTViewPanelController',
'PhabricatorXHProfApplication' => 'PhabricatorApplication',
'PhabricatorXHProfController' => 'PhabricatorController',
'PhabricatorXHProfDAO' => 'PhabricatorLiskDAO',
'PhabricatorXHProfDropController' => 'PhabricatorXHProfController',
'PhabricatorXHProfProfileController' => 'PhabricatorXHProfController',
'PhabricatorXHProfProfileSymbolView' => 'PhabricatorXHProfProfileView',
'PhabricatorXHProfProfileTopLevelView' => 'PhabricatorXHProfProfileView',
'PhabricatorXHProfProfileView' => 'AphrontView',
'PhabricatorXHProfSample' => array(
'PhabricatorXHProfDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorXHProfSampleListController' => 'PhabricatorXHProfController',
'PhabricatorXHProfSampleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorXHProfSampleSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorYoutubeRemarkupRule' => 'PhutilRemarkupRule',
'Phame404Response' => 'AphrontHTMLResponse',
'PhameBlog' => array(
'PhameDAO',
'PhabricatorPolicyInterface',
'PhabricatorMarkupInterface',
'PhabricatorSubscribableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorConduitResultInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
),
'PhameBlog404Controller' => 'PhameLiveController',
'PhameBlogArchiveController' => 'PhameBlogController',
'PhameBlogController' => 'PhameController',
'PhameBlogCreateCapability' => 'PhabricatorPolicyCapability',
'PhameBlogDatasource' => 'PhabricatorTypeaheadDatasource',
'PhameBlogDescriptionTransaction' => 'PhameBlogTransactionType',
'PhameBlogEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhameBlogEditController' => 'PhameBlogController',
'PhameBlogEditEngine' => 'PhabricatorEditEngine',
'PhameBlogEditor' => 'PhabricatorApplicationTransactionEditor',
'PhameBlogFeedController' => 'PhameBlogController',
'PhameBlogFerretEngine' => 'PhabricatorFerretEngine',
'PhameBlogFullDomainTransaction' => 'PhameBlogTransactionType',
'PhameBlogFulltextEngine' => 'PhabricatorFulltextEngine',
'PhameBlogHeaderImageTransaction' => 'PhameBlogTransactionType',
'PhameBlogHeaderPictureController' => 'PhameBlogController',
'PhameBlogListController' => 'PhameBlogController',
'PhameBlogListView' => 'AphrontTagView',
'PhameBlogManageController' => 'PhameBlogController',
'PhameBlogNameTransaction' => 'PhameBlogTransactionType',
'PhameBlogParentDomainTransaction' => 'PhameBlogTransactionType',
'PhameBlogParentSiteTransaction' => 'PhameBlogTransactionType',
'PhameBlogProfileImageTransaction' => 'PhameBlogTransactionType',
'PhameBlogProfilePictureController' => 'PhameBlogController',
'PhameBlogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhameBlogReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhameBlogSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhameBlogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhameBlogSite' => 'PhameSite',
'PhameBlogStatusTransaction' => 'PhameBlogTransactionType',
'PhameBlogSubtitleTransaction' => 'PhameBlogTransactionType',
'PhameBlogTransaction' => 'PhabricatorModularTransaction',
'PhameBlogTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhameBlogTransactionType' => 'PhabricatorModularTransactionType',
'PhameBlogViewController' => 'PhameLiveController',
'PhameConstants' => 'Phobject',
'PhameController' => 'PhabricatorController',
'PhameDAO' => 'PhabricatorLiskDAO',
'PhameDescriptionView' => 'AphrontTagView',
'PhameDraftListView' => 'AphrontTagView',
'PhameHomeController' => 'PhamePostController',
'PhameLiveController' => 'PhameController',
'PhameNextPostView' => 'AphrontTagView',
'PhamePost' => array(
'PhameDAO',
'PhabricatorPolicyInterface',
'PhabricatorMarkupInterface',
'PhabricatorFlaggableInterface',
'PhabricatorProjectInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorSubscribableInterface',
'PhabricatorDestructibleInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorConduitResultInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
),
'PhamePostArchiveController' => 'PhamePostController',
'PhamePostBlogTransaction' => 'PhamePostTransactionType',
'PhamePostBodyTransaction' => 'PhamePostTransactionType',
'PhamePostController' => 'PhameController',
'PhamePostEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhamePostEditController' => 'PhamePostController',
'PhamePostEditEngine' => 'PhabricatorEditEngine',
'PhamePostEditor' => 'PhabricatorApplicationTransactionEditor',
'PhamePostFerretEngine' => 'PhabricatorFerretEngine',
'PhamePostFulltextEngine' => 'PhabricatorFulltextEngine',
'PhamePostHeaderImageTransaction' => 'PhamePostTransactionType',
'PhamePostHeaderPictureController' => 'PhamePostController',
'PhamePostHistoryController' => 'PhamePostController',
'PhamePostListController' => 'PhamePostController',
'PhamePostListView' => 'AphrontTagView',
'PhamePostMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhamePostMoveController' => 'PhamePostController',
'PhamePostPublishController' => 'PhamePostController',
'PhamePostQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhamePostRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhamePostReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhamePostSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhamePostSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhamePostSubtitleTransaction' => 'PhamePostTransactionType',
'PhamePostTitleTransaction' => 'PhamePostTransactionType',
'PhamePostTransaction' => 'PhabricatorModularTransaction',
'PhamePostTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhamePostTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhamePostTransactionType' => 'PhabricatorModularTransactionType',
'PhamePostViewController' => 'PhameLiveController',
'PhamePostVisibilityTransaction' => 'PhamePostTransactionType',
'PhameSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhameSite' => 'PhabricatorSite',
'PhluxController' => 'PhabricatorController',
'PhluxDAO' => 'PhabricatorLiskDAO',
'PhluxEditController' => 'PhluxController',
'PhluxListController' => 'PhluxController',
'PhluxSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhluxTransaction' => 'PhabricatorApplicationTransaction',
'PhluxTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhluxVariable' => array(
'PhluxDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorFlaggableInterface',
'PhabricatorPolicyInterface',
),
'PhluxVariableEditor' => 'PhabricatorApplicationTransactionEditor',
'PhluxVariablePHIDType' => 'PhabricatorPHIDType',
'PhluxVariableQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhluxViewController' => 'PhluxController',
'PholioController' => 'PhabricatorController',
'PholioDAO' => 'PhabricatorLiskDAO',
'PholioDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PholioDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PholioImage' => array(
'PholioDAO',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
),
'PholioImageDescriptionTransaction' => 'PholioImageTransactionType',
'PholioImageFileTransaction' => 'PholioImageTransactionType',
'PholioImageNameTransaction' => 'PholioImageTransactionType',
'PholioImagePHIDType' => 'PhabricatorPHIDType',
'PholioImageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PholioImageReplaceTransaction' => 'PholioImageTransactionType',
'PholioImageSequenceTransaction' => 'PholioImageTransactionType',
'PholioImageTransactionType' => 'PholioTransactionType',
'PholioImageUploadController' => 'PholioController',
'PholioInlineController' => 'PholioController',
'PholioInlineListController' => 'PholioController',
'PholioMock' => array(
'PholioDAO',
'PhabricatorPolicyInterface',
'PhabricatorSubscribableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorFlaggableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorTimelineInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSpacesInterface',
'PhabricatorMentionableInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
),
'PholioMockArchiveController' => 'PholioController',
'PholioMockAuthorHeraldField' => 'PholioMockHeraldField',
'PholioMockCommentController' => 'PholioController',
'PholioMockDescriptionHeraldField' => 'PholioMockHeraldField',
'PholioMockDescriptionTransaction' => 'PholioMockTransactionType',
'PholioMockEditController' => 'PholioController',
'PholioMockEditor' => 'PhabricatorApplicationTransactionEditor',
'PholioMockEmbedView' => 'AphrontView',
'PholioMockFerretEngine' => 'PhabricatorFerretEngine',
'PholioMockFulltextEngine' => 'PhabricatorFulltextEngine',
'PholioMockHasTaskEdgeType' => 'PhabricatorEdgeType',
'PholioMockHasTaskRelationship' => 'PholioMockRelationship',
'PholioMockHeraldField' => 'HeraldField',
'PholioMockHeraldFieldGroup' => 'HeraldFieldGroup',
'PholioMockImagesView' => 'AphrontView',
'PholioMockInlineTransaction' => 'PholioMockTransactionType',
'PholioMockListController' => 'PholioController',
'PholioMockMailReceiver' => 'PhabricatorObjectMailReceiver',
'PholioMockNameHeraldField' => 'PholioMockHeraldField',
'PholioMockNameTransaction' => 'PholioMockTransactionType',
'PholioMockPHIDType' => 'PhabricatorPHIDType',
'PholioMockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PholioMockRelationship' => 'PhabricatorObjectRelationship',
'PholioMockRelationshipSource' => 'PhabricatorObjectRelationshipSource',
'PholioMockSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PholioMockStatusTransaction' => 'PholioMockTransactionType',
'PholioMockThumbGridView' => 'AphrontView',
'PholioMockTimelineEngine' => 'PhabricatorTimelineEngine',
'PholioMockTransactionType' => 'PholioTransactionType',
'PholioMockViewController' => 'PholioController',
'PholioRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PholioReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PholioSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PholioTransaction' => 'PhabricatorModularTransaction',
'PholioTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PholioTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PholioTransactionType' => 'PhabricatorModularTransactionType',
'PholioTransactionView' => 'PhabricatorApplicationTransactionView',
'PholioUploadedImageView' => 'AphrontView',
'PhortuneAccount' => array(
'PhortuneDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'PhortuneAccountAddManagerController' => 'PhortuneController',
'PhortuneAccountBillingAddressTransaction' => 'PhortuneAccountTransactionType',
'PhortuneAccountBillingController' => 'PhortuneAccountProfileController',
'PhortuneAccountBillingNameTransaction' => 'PhortuneAccountTransactionType',
'PhortuneAccountChargeListController' => 'PhortuneController',
'PhortuneAccountController' => 'PhortuneController',
'PhortuneAccountEditController' => 'PhortuneController',
'PhortuneAccountEditEngine' => 'PhabricatorEditEngine',
'PhortuneAccountEditor' => 'PhabricatorApplicationTransactionEditor',
'PhortuneAccountHasMemberEdgeType' => 'PhabricatorEdgeType',
'PhortuneAccountListController' => 'PhortuneController',
'PhortuneAccountManagerController' => 'PhortuneAccountProfileController',
'PhortuneAccountNameTransaction' => 'PhortuneAccountTransactionType',
'PhortuneAccountPHIDType' => 'PhabricatorPHIDType',
'PhortuneAccountProfileController' => 'PhortuneAccountController',
'PhortuneAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneAccountSubscriptionController' => 'PhortuneAccountProfileController',
'PhortuneAccountTransaction' => 'PhabricatorModularTransaction',
'PhortuneAccountTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhortuneAccountTransactionType' => 'PhabricatorModularTransactionType',
'PhortuneAccountViewController' => 'PhortuneAccountProfileController',
'PhortuneAdHocCart' => 'PhortuneCartImplementation',
'PhortuneAdHocProduct' => 'PhortuneProductImplementation',
+ 'PhortuneAddPaymentMethodAction' => 'PhabricatorSystemAction',
'PhortuneCart' => array(
'PhortuneDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'PhortuneCartAcceptController' => 'PhortuneCartController',
'PhortuneCartCancelController' => 'PhortuneCartController',
'PhortuneCartCheckoutController' => 'PhortuneCartController',
'PhortuneCartController' => 'PhortuneController',
'PhortuneCartEditor' => 'PhabricatorApplicationTransactionEditor',
'PhortuneCartImplementation' => 'Phobject',
'PhortuneCartListController' => 'PhortuneController',
'PhortuneCartPHIDType' => 'PhabricatorPHIDType',
'PhortuneCartQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneCartReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhortuneCartSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhortuneCartTransaction' => 'PhabricatorApplicationTransaction',
'PhortuneCartTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhortuneCartUpdateController' => 'PhortuneCartController',
'PhortuneCartViewController' => 'PhortuneCartController',
'PhortuneCharge' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
),
'PhortuneChargePHIDType' => 'PhabricatorPHIDType',
'PhortuneChargeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneChargeSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhortuneChargeTableView' => 'AphrontView',
'PhortuneConstants' => 'Phobject',
'PhortuneController' => 'PhabricatorController',
'PhortuneCreditCardForm' => 'Phobject',
'PhortuneCurrency' => 'Phobject',
'PhortuneCurrencySerializer' => 'PhabricatorLiskSerializer',
'PhortuneCurrencyTestCase' => 'PhabricatorTestCase',
'PhortuneDAO' => 'PhabricatorLiskDAO',
+ 'PhortuneDisplayException' => 'Exception',
'PhortuneErrCode' => 'PhortuneConstants',
'PhortuneInvoiceView' => 'AphrontTagView',
'PhortuneLandingController' => 'PhortuneController',
'PhortuneMemberHasAccountEdgeType' => 'PhabricatorEdgeType',
'PhortuneMemberHasMerchantEdgeType' => 'PhabricatorEdgeType',
'PhortuneMerchant' => array(
'PhortuneDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'PhortuneMerchantAddManagerController' => 'PhortuneController',
'PhortuneMerchantCapability' => 'PhabricatorPolicyCapability',
'PhortuneMerchantContactInfoTransaction' => 'PhortuneMerchantTransactionType',
'PhortuneMerchantController' => 'PhortuneController',
'PhortuneMerchantDescriptionTransaction' => 'PhortuneMerchantTransactionType',
'PhortuneMerchantEditController' => 'PhortuneMerchantController',
'PhortuneMerchantEditEngine' => 'PhabricatorEditEngine',
'PhortuneMerchantEditor' => 'PhabricatorApplicationTransactionEditor',
'PhortuneMerchantHasMemberEdgeType' => 'PhabricatorEdgeType',
'PhortuneMerchantInvoiceCreateController' => 'PhortuneMerchantProfileController',
'PhortuneMerchantInvoiceEmailTransaction' => 'PhortuneMerchantTransactionType',
'PhortuneMerchantInvoiceFooterTransaction' => 'PhortuneMerchantTransactionType',
'PhortuneMerchantListController' => 'PhortuneMerchantController',
'PhortuneMerchantManagerController' => 'PhortuneMerchantProfileController',
'PhortuneMerchantNameTransaction' => 'PhortuneMerchantTransactionType',
'PhortuneMerchantPHIDType' => 'PhabricatorPHIDType',
'PhortuneMerchantPictureController' => 'PhortuneMerchantProfileController',
'PhortuneMerchantPictureTransaction' => 'PhortuneMerchantTransactionType',
'PhortuneMerchantProfileController' => 'PhortuneController',
'PhortuneMerchantQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneMerchantSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhortuneMerchantTransaction' => 'PhabricatorModularTransaction',
'PhortuneMerchantTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhortuneMerchantTransactionType' => 'PhabricatorModularTransactionType',
'PhortuneMerchantViewController' => 'PhortuneMerchantProfileController',
'PhortuneMonthYearExpiryControl' => 'AphrontFormControl',
'PhortuneOrderTableView' => 'AphrontView',
'PhortunePayPalPaymentProvider' => 'PhortunePaymentProvider',
'PhortunePaymentMethod' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
),
'PhortunePaymentMethodCreateController' => 'PhortuneController',
'PhortunePaymentMethodDisableController' => 'PhortuneController',
'PhortunePaymentMethodEditController' => 'PhortuneController',
'PhortunePaymentMethodPHIDType' => 'PhabricatorPHIDType',
'PhortunePaymentMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortunePaymentProvider' => 'Phobject',
'PhortunePaymentProviderConfig' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhortunePaymentProviderConfigEditor' => 'PhabricatorApplicationTransactionEditor',
'PhortunePaymentProviderConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortunePaymentProviderConfigTransaction' => 'PhabricatorApplicationTransaction',
'PhortunePaymentProviderConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhortunePaymentProviderPHIDType' => 'PhabricatorPHIDType',
'PhortunePaymentProviderTestCase' => 'PhabricatorTestCase',
'PhortuneProduct' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
),
'PhortuneProductImplementation' => 'Phobject',
'PhortuneProductListController' => 'PhabricatorController',
'PhortuneProductPHIDType' => 'PhabricatorPHIDType',
'PhortuneProductQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneProductViewController' => 'PhortuneController',
'PhortuneProviderActionController' => 'PhortuneController',
'PhortuneProviderDisableController' => 'PhortuneMerchantController',
'PhortuneProviderEditController' => 'PhortuneMerchantController',
'PhortunePurchase' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
),
'PhortunePurchasePHIDType' => 'PhabricatorPHIDType',
'PhortunePurchaseQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhortuneStripePaymentProvider' => 'PhortunePaymentProvider',
'PhortuneSubscription' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
),
'PhortuneSubscriptionCart' => 'PhortuneCartImplementation',
'PhortuneSubscriptionEditController' => 'PhortuneController',
'PhortuneSubscriptionImplementation' => 'Phobject',
'PhortuneSubscriptionListController' => 'PhortuneController',
'PhortuneSubscriptionPHIDType' => 'PhabricatorPHIDType',
'PhortuneSubscriptionProduct' => 'PhortuneProductImplementation',
'PhortuneSubscriptionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneSubscriptionSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhortuneSubscriptionTableView' => 'AphrontView',
'PhortuneSubscriptionViewController' => 'PhortuneController',
'PhortuneSubscriptionWorker' => 'PhabricatorWorker',
'PhortuneTestPaymentProvider' => 'PhortunePaymentProvider',
'PhortuneWePayPaymentProvider' => 'PhortunePaymentProvider',
'PhragmentBrowseController' => 'PhragmentController',
'PhragmentCanCreateCapability' => 'PhabricatorPolicyCapability',
'PhragmentConduitAPIMethod' => 'ConduitAPIMethod',
'PhragmentController' => 'PhabricatorController',
'PhragmentCreateController' => 'PhragmentController',
'PhragmentDAO' => 'PhabricatorLiskDAO',
'PhragmentFragment' => array(
'PhragmentDAO',
'PhabricatorPolicyInterface',
),
'PhragmentFragmentPHIDType' => 'PhabricatorPHIDType',
'PhragmentFragmentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhragmentFragmentVersion' => array(
'PhragmentDAO',
'PhabricatorPolicyInterface',
),
'PhragmentFragmentVersionPHIDType' => 'PhabricatorPHIDType',
'PhragmentFragmentVersionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhragmentGetPatchConduitAPIMethod' => 'PhragmentConduitAPIMethod',
'PhragmentHistoryController' => 'PhragmentController',
'PhragmentPatchController' => 'PhragmentController',
'PhragmentPatchUtil' => 'Phobject',
'PhragmentPolicyController' => 'PhragmentController',
'PhragmentQueryFragmentsConduitAPIMethod' => 'PhragmentConduitAPIMethod',
'PhragmentRevertController' => 'PhragmentController',
'PhragmentSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhragmentSnapshot' => array(
'PhragmentDAO',
'PhabricatorPolicyInterface',
),
'PhragmentSnapshotChild' => array(
'PhragmentDAO',
'PhabricatorPolicyInterface',
),
'PhragmentSnapshotChildQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhragmentSnapshotCreateController' => 'PhragmentController',
'PhragmentSnapshotDeleteController' => 'PhragmentController',
'PhragmentSnapshotPHIDType' => 'PhabricatorPHIDType',
'PhragmentSnapshotPromoteController' => 'PhragmentController',
'PhragmentSnapshotQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhragmentSnapshotViewController' => 'PhragmentController',
'PhragmentUpdateController' => 'PhragmentController',
'PhragmentVersionController' => 'PhragmentController',
'PhragmentZIPController' => 'PhragmentController',
'PhrequentConduitAPIMethod' => 'ConduitAPIMethod',
'PhrequentController' => 'PhabricatorController',
'PhrequentCurtainExtension' => 'PHUICurtainExtension',
'PhrequentDAO' => 'PhabricatorLiskDAO',
'PhrequentListController' => 'PhrequentController',
'PhrequentPopConduitAPIMethod' => 'PhrequentConduitAPIMethod',
'PhrequentPushConduitAPIMethod' => 'PhrequentConduitAPIMethod',
'PhrequentSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhrequentTimeBlock' => 'Phobject',
'PhrequentTimeBlockTestCase' => 'PhabricatorTestCase',
'PhrequentTimeSlices' => 'Phobject',
'PhrequentTrackController' => 'PhrequentController',
'PhrequentTrackingConduitAPIMethod' => 'PhrequentConduitAPIMethod',
'PhrequentTrackingEditor' => 'PhabricatorEditor',
'PhrequentUIEventListener' => 'PhabricatorEventListener',
'PhrequentUserTime' => array(
'PhrequentDAO',
'PhabricatorPolicyInterface',
),
'PhrequentUserTimeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhrictionChangeType' => 'PhrictionConstants',
'PhrictionConduitAPIMethod' => 'ConduitAPIMethod',
'PhrictionConstants' => 'Phobject',
'PhrictionContent' => array(
'PhrictionDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
),
'PhrictionContentPHIDType' => 'PhabricatorPHIDType',
'PhrictionContentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhrictionContentSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhrictionContentSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhrictionContentSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhrictionController' => 'PhabricatorController',
'PhrictionCreateConduitAPIMethod' => 'PhrictionConduitAPIMethod',
'PhrictionDAO' => 'PhabricatorLiskDAO',
'PhrictionDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension',
'PhrictionDeleteController' => 'PhrictionController',
'PhrictionDiffController' => 'PhrictionController',
'PhrictionDocument' => array(
'PhrictionDAO',
'PhabricatorPolicyInterface',
'PhabricatorSubscribableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorDestructibleInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'PhabricatorProjectInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorConduitResultInterface',
'PhabricatorPolicyCodexInterface',
'PhabricatorSpacesInterface',
),
'PhrictionDocumentAuthorHeraldField' => 'PhrictionDocumentHeraldField',
'PhrictionDocumentContentHeraldField' => 'PhrictionDocumentHeraldField',
'PhrictionDocumentContentTransaction' => 'PhrictionDocumentEditTransaction',
'PhrictionDocumentController' => 'PhrictionController',
'PhrictionDocumentDatasource' => 'PhabricatorTypeaheadDatasource',
'PhrictionDocumentDeleteTransaction' => 'PhrictionDocumentVersionTransaction',
'PhrictionDocumentDraftTransaction' => 'PhrictionDocumentEditTransaction',
'PhrictionDocumentEditEngine' => 'PhabricatorEditEngine',
'PhrictionDocumentEditTransaction' => 'PhrictionDocumentVersionTransaction',
'PhrictionDocumentFerretEngine' => 'PhabricatorFerretEngine',
'PhrictionDocumentFulltextEngine' => 'PhabricatorFulltextEngine',
'PhrictionDocumentHeraldAdapter' => 'HeraldAdapter',
'PhrictionDocumentHeraldField' => 'HeraldField',
'PhrictionDocumentHeraldFieldGroup' => 'HeraldFieldGroup',
'PhrictionDocumentMoveAwayTransaction' => 'PhrictionDocumentVersionTransaction',
'PhrictionDocumentMoveToTransaction' => 'PhrictionDocumentVersionTransaction',
'PhrictionDocumentPHIDType' => 'PhabricatorPHIDType',
'PhrictionDocumentPathHeraldField' => 'PhrictionDocumentHeraldField',
'PhrictionDocumentPolicyCodex' => 'PhabricatorPolicyCodex',
'PhrictionDocumentPublishTransaction' => 'PhrictionDocumentTransactionType',
'PhrictionDocumentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhrictionDocumentSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhrictionDocumentSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhrictionDocumentStatus' => 'PhabricatorObjectStatus',
'PhrictionDocumentTitleHeraldField' => 'PhrictionDocumentHeraldField',
'PhrictionDocumentTitleTransaction' => 'PhrictionDocumentVersionTransaction',
'PhrictionDocumentTransactionType' => 'PhabricatorModularTransactionType',
'PhrictionDocumentVersionTransaction' => 'PhrictionDocumentTransactionType',
'PhrictionEditConduitAPIMethod' => 'PhrictionConduitAPIMethod',
'PhrictionEditController' => 'PhrictionController',
'PhrictionEditEngineController' => 'PhrictionController',
'PhrictionHistoryConduitAPIMethod' => 'PhrictionConduitAPIMethod',
'PhrictionHistoryController' => 'PhrictionController',
'PhrictionInfoConduitAPIMethod' => 'PhrictionConduitAPIMethod',
'PhrictionListController' => 'PhrictionController',
'PhrictionMarkupPreviewController' => 'PhabricatorController',
'PhrictionMoveController' => 'PhrictionController',
'PhrictionNewController' => 'PhrictionController',
'PhrictionPublishController' => 'PhrictionController',
'PhrictionRemarkupRule' => 'PhutilRemarkupRule',
'PhrictionReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhrictionSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhrictionTransaction' => 'PhabricatorModularTransaction',
'PhrictionTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhrictionTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhrictionTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PolicyLockOptionType' => 'PhabricatorConfigJSONOptionType',
'PonderAddAnswerView' => 'AphrontView',
'PonderAnswer' => array(
'PonderDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorMarkupInterface',
'PhabricatorPolicyInterface',
'PhabricatorFlaggableInterface',
'PhabricatorSubscribableInterface',
'PhabricatorDestructibleInterface',
),
'PonderAnswerCommentController' => 'PonderController',
'PonderAnswerContentTransaction' => 'PonderAnswerTransactionType',
'PonderAnswerEditController' => 'PonderController',
'PonderAnswerEditor' => 'PonderEditor',
'PonderAnswerHistoryController' => 'PonderController',
'PonderAnswerMailReceiver' => 'PhabricatorObjectMailReceiver',
'PonderAnswerPHIDType' => 'PhabricatorPHIDType',
'PonderAnswerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PonderAnswerQuestionIDTransaction' => 'PonderAnswerTransactionType',
'PonderAnswerReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PonderAnswerSaveController' => 'PonderController',
'PonderAnswerStatus' => 'PonderConstants',
'PonderAnswerStatusTransaction' => 'PonderAnswerTransactionType',
'PonderAnswerTransaction' => 'PhabricatorModularTransaction',
'PonderAnswerTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PonderAnswerTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PonderAnswerTransactionType' => 'PhabricatorModularTransactionType',
'PonderAnswerView' => 'AphrontTagView',
'PonderConstants' => 'Phobject',
'PonderController' => 'PhabricatorController',
'PonderDAO' => 'PhabricatorLiskDAO',
'PonderDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PonderEditor' => 'PhabricatorApplicationTransactionEditor',
'PonderFooterView' => 'AphrontTagView',
'PonderModerateCapability' => 'PhabricatorPolicyCapability',
'PonderQuestion' => array(
'PonderDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorMarkupInterface',
'PhabricatorSubscribableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorPolicyInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSpacesInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
),
'PonderQuestionAnswerTransaction' => 'PonderQuestionTransactionType',
'PonderQuestionAnswerWikiTransaction' => 'PonderQuestionTransactionType',
'PonderQuestionCommentController' => 'PonderController',
'PonderQuestionContentTransaction' => 'PonderQuestionTransactionType',
'PonderQuestionCreateMailReceiver' => 'PhabricatorApplicationMailReceiver',
'PonderQuestionEditController' => 'PonderController',
'PonderQuestionEditEngine' => 'PhabricatorEditEngine',
'PonderQuestionEditor' => 'PonderEditor',
'PonderQuestionFerretEngine' => 'PhabricatorFerretEngine',
'PonderQuestionFulltextEngine' => 'PhabricatorFulltextEngine',
'PonderQuestionHistoryController' => 'PonderController',
'PonderQuestionListController' => 'PonderController',
'PonderQuestionMailReceiver' => 'PhabricatorObjectMailReceiver',
'PonderQuestionPHIDType' => 'PhabricatorPHIDType',
'PonderQuestionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PonderQuestionReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PonderQuestionSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PonderQuestionStatus' => 'PonderConstants',
'PonderQuestionStatusController' => 'PonderController',
'PonderQuestionStatusTransaction' => 'PonderQuestionTransactionType',
'PonderQuestionTitleTransaction' => 'PonderQuestionTransactionType',
'PonderQuestionTransaction' => 'PhabricatorModularTransaction',
'PonderQuestionTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PonderQuestionTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PonderQuestionTransactionType' => 'PhabricatorModularTransactionType',
'PonderQuestionViewController' => 'PonderController',
'PonderRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PonderSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'ProjectAddProjectsEmailCommand' => 'MetaMTAEmailTransactionCommand',
'ProjectBoardTaskCard' => 'Phobject',
'ProjectCanLockProjectsCapability' => 'PhabricatorPolicyCapability',
'ProjectColumnSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'ProjectConduitAPIMethod' => 'ConduitAPIMethod',
'ProjectCreateConduitAPIMethod' => 'ProjectConduitAPIMethod',
'ProjectCreateProjectsCapability' => 'PhabricatorPolicyCapability',
'ProjectDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension',
'ProjectDefaultEditCapability' => 'PhabricatorPolicyCapability',
'ProjectDefaultJoinCapability' => 'PhabricatorPolicyCapability',
'ProjectDefaultViewCapability' => 'PhabricatorPolicyCapability',
'ProjectEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'ProjectQueryConduitAPIMethod' => 'ProjectConduitAPIMethod',
'ProjectRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'ProjectRemarkupRuleTestCase' => 'PhabricatorTestCase',
'ProjectReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'ProjectSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'QueryFormattingTestCase' => 'PhabricatorTestCase',
'ReleephAuthorFieldSpecification' => 'ReleephFieldSpecification',
'ReleephBranch' => array(
'ReleephDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'ReleephBranchAccessController' => 'ReleephBranchController',
'ReleephBranchCommitFieldSpecification' => 'ReleephFieldSpecification',
'ReleephBranchController' => 'ReleephController',
'ReleephBranchCreateController' => 'ReleephProductController',
'ReleephBranchEditController' => 'ReleephBranchController',
'ReleephBranchEditor' => 'PhabricatorEditor',
'ReleephBranchHistoryController' => 'ReleephBranchController',
'ReleephBranchNamePreviewController' => 'ReleephController',
'ReleephBranchPHIDType' => 'PhabricatorPHIDType',
'ReleephBranchPreviewView' => 'AphrontFormControl',
'ReleephBranchQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'ReleephBranchSearchEngine' => 'PhabricatorApplicationSearchEngine',
'ReleephBranchTemplate' => 'Phobject',
'ReleephBranchTransaction' => 'PhabricatorApplicationTransaction',
'ReleephBranchTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'ReleephBranchViewController' => 'ReleephBranchController',
'ReleephCommitFinder' => 'Phobject',
'ReleephCommitFinderException' => 'Exception',
'ReleephCommitMessageFieldSpecification' => 'ReleephFieldSpecification',
'ReleephConduitAPIMethod' => 'ConduitAPIMethod',
'ReleephController' => 'PhabricatorController',
'ReleephDAO' => 'PhabricatorLiskDAO',
'ReleephDefaultFieldSelector' => 'ReleephFieldSelector',
'ReleephDependsOnFieldSpecification' => 'ReleephFieldSpecification',
'ReleephDiffChurnFieldSpecification' => 'ReleephFieldSpecification',
'ReleephDiffMessageFieldSpecification' => 'ReleephFieldSpecification',
'ReleephDiffSizeFieldSpecification' => 'ReleephFieldSpecification',
'ReleephFieldParseException' => 'Exception',
'ReleephFieldSelector' => 'Phobject',
'ReleephFieldSpecification' => array(
'PhabricatorCustomField',
'PhabricatorMarkupInterface',
),
'ReleephGetBranchesConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephIntentFieldSpecification' => 'ReleephFieldSpecification',
'ReleephLevelFieldSpecification' => 'ReleephFieldSpecification',
'ReleephOriginalCommitFieldSpecification' => 'ReleephFieldSpecification',
'ReleephProductActionController' => 'ReleephProductController',
'ReleephProductController' => 'ReleephController',
'ReleephProductCreateController' => 'ReleephProductController',
'ReleephProductEditController' => 'ReleephProductController',
'ReleephProductEditor' => 'PhabricatorApplicationTransactionEditor',
'ReleephProductHistoryController' => 'ReleephProductController',
'ReleephProductListController' => 'ReleephController',
'ReleephProductPHIDType' => 'PhabricatorPHIDType',
'ReleephProductQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'ReleephProductSearchEngine' => 'PhabricatorApplicationSearchEngine',
'ReleephProductTransaction' => 'PhabricatorApplicationTransaction',
'ReleephProductTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'ReleephProductViewController' => 'ReleephProductController',
'ReleephProject' => array(
'ReleephDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'ReleephQueryBranchesConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephQueryProductsConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephQueryRequestsConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephReasonFieldSpecification' => 'ReleephFieldSpecification',
'ReleephRequest' => array(
'ReleephDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorCustomFieldInterface',
),
'ReleephRequestActionController' => 'ReleephRequestController',
'ReleephRequestCommentController' => 'ReleephRequestController',
'ReleephRequestConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephRequestController' => 'ReleephController',
'ReleephRequestDifferentialCreateController' => 'ReleephController',
'ReleephRequestEditController' => 'ReleephBranchController',
'ReleephRequestMailReceiver' => 'PhabricatorObjectMailReceiver',
'ReleephRequestPHIDType' => 'PhabricatorPHIDType',
'ReleephRequestQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'ReleephRequestReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'ReleephRequestSearchEngine' => 'PhabricatorApplicationSearchEngine',
'ReleephRequestStatus' => 'Phobject',
'ReleephRequestTransaction' => 'PhabricatorApplicationTransaction',
'ReleephRequestTransactionComment' => 'PhabricatorApplicationTransactionComment',
'ReleephRequestTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'ReleephRequestTransactionalEditor' => 'PhabricatorApplicationTransactionEditor',
'ReleephRequestTypeaheadControl' => 'AphrontFormControl',
'ReleephRequestTypeaheadController' => 'PhabricatorTypeaheadDatasourceController',
'ReleephRequestView' => 'AphrontView',
'ReleephRequestViewController' => 'ReleephBranchController',
'ReleephRequestorFieldSpecification' => 'ReleephFieldSpecification',
'ReleephRevisionFieldSpecification' => 'ReleephFieldSpecification',
'ReleephSeverityFieldSpecification' => 'ReleephLevelFieldSpecification',
'ReleephSummaryFieldSpecification' => 'ReleephFieldSpecification',
'ReleephWorkCanPushConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephWorkGetAuthorInfoConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephWorkGetBranchCommitMessageConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephWorkGetBranchConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephWorkGetCommitMessageConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephWorkNextRequestConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephWorkRecordConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephWorkRecordPickStatusConduitAPIMethod' => 'ReleephConduitAPIMethod',
'RemarkupProcessConduitAPIMethod' => 'ConduitAPIMethod',
'RepositoryConduitAPIMethod' => 'ConduitAPIMethod',
'RepositoryQueryConduitAPIMethod' => 'RepositoryConduitAPIMethod',
'ShellLogView' => 'AphrontView',
'SlowvoteConduitAPIMethod' => 'ConduitAPIMethod',
'SlowvoteEmbedView' => 'AphrontView',
'SlowvoteInfoConduitAPIMethod' => 'SlowvoteConduitAPIMethod',
'SlowvoteRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'SubscriptionListDialogBuilder' => 'Phobject',
'SubscriptionListStringBuilder' => 'Phobject',
'TokenConduitAPIMethod' => 'ConduitAPIMethod',
'TokenGiveConduitAPIMethod' => 'TokenConduitAPIMethod',
'TokenGivenConduitAPIMethod' => 'TokenConduitAPIMethod',
'TokenQueryConduitAPIMethod' => 'TokenConduitAPIMethod',
'TransactionSearchConduitAPIMethod' => 'ConduitAPIMethod',
'UserConduitAPIMethod' => 'ConduitAPIMethod',
'UserDisableConduitAPIMethod' => 'UserConduitAPIMethod',
'UserEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'UserEnableConduitAPIMethod' => 'UserConduitAPIMethod',
'UserFindConduitAPIMethod' => 'UserConduitAPIMethod',
'UserQueryConduitAPIMethod' => 'UserConduitAPIMethod',
'UserSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'UserWhoAmIConduitAPIMethod' => 'UserConduitAPIMethod',
),
));
diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php
index 78e6dac9d..48004a521 100644
--- a/src/aphront/AphrontRequest.php
+++ b/src/aphront/AphrontRequest.php
@@ -1,905 +1,909 @@
<?php
/**
* @task data Accessing Request Data
* @task cookie Managing Cookies
* @task cluster Working With a Phabricator Cluster
*/
final class AphrontRequest extends Phobject {
// NOTE: These magic request-type parameters are automatically included in
// certain requests (e.g., by phabricator_form(), JX.Request,
// JX.Workflow, and ConduitClient) and help us figure out what sort of
// response the client expects.
const TYPE_AJAX = '__ajax__';
const TYPE_FORM = '__form__';
const TYPE_CONDUIT = '__conduit__';
const TYPE_WORKFLOW = '__wflow__';
const TYPE_CONTINUE = '__continue__';
const TYPE_PREVIEW = '__preview__';
const TYPE_HISEC = '__hisec__';
const TYPE_QUICKSAND = '__quicksand__';
private $host;
private $path;
private $requestData;
private $user;
private $applicationConfiguration;
private $site;
private $controller;
private $uriData = array();
private $cookiePrefix;
public function __construct($host, $path) {
$this->host = $host;
$this->path = $path;
}
public function setURIMap(array $uri_data) {
$this->uriData = $uri_data;
return $this;
}
public function getURIMap() {
return $this->uriData;
}
public function getURIData($key, $default = null) {
return idx($this->uriData, $key, $default);
}
/**
* Read line range parameter data from the request.
*
* Applications like Paste, Diffusion, and Harbormaster use "$12-14" in the
* URI to allow users to link to particular lines.
*
* @param string URI data key to pull line range information from.
* @param int|null Maximum length of the range.
* @return null|pair<int, int> Null, or beginning and end of the range.
*/
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;
}
$range = explode('-', $range, 2);
foreach ($range as $key => $value) {
$value = (int)$value;
if (!$value) {
// If either value is "0", discard the range.
return null;
}
$range[$key] = $value;
}
// If the range is like "$10", treat it like "$10-10".
if (count($range) == 1) {
$range[] = head($range);
}
// If the range is "$7-5", treat it like "$5-7".
if ($range[1] < $range[0]) {
$range = array_reverse($range);
}
// If the user specified something like "$1-999999999" and we have a limit,
// clamp it to a more reasonable range.
if ($limit !== null) {
if ($range[1] - $range[0] > $limit) {
$range[1] = $range[0] + $limit;
}
}
return $range;
}
public function setApplicationConfiguration(
$application_configuration) {
$this->applicationConfiguration = $application_configuration;
return $this;
}
public function getApplicationConfiguration() {
return $this->applicationConfiguration;
}
public function setPath($path) {
$this->path = $path;
return $this;
}
public function getPath() {
return $this->path;
}
public function getHost() {
// The "Host" header may include a port number, or may be a malicious
// header in the form "realdomain.com:ignored@evil.com". Invoke the full
// parser to extract the real domain correctly. See here for coverage of
// a similar issue in Django:
//
// https://www.djangoproject.com/weblog/2012/oct/17/security/
$uri = new PhutilURI('http://'.$this->host);
return $uri->getDomain();
}
public function setSite(AphrontSite $site) {
$this->site = $site;
return $this;
}
public function getSite() {
return $this->site;
}
public function setController(AphrontController $controller) {
$this->controller = $controller;
return $this;
}
public function getController() {
return $this->controller;
}
/* -( Accessing Request Data )--------------------------------------------- */
/**
* @task data
*/
public function setRequestData(array $request_data) {
$this->requestData = $request_data;
return $this;
}
/**
* @task data
*/
public function getRequestData() {
return $this->requestData;
}
/**
* @task data
*/
public function getInt($name, $default = null) {
if (isset($this->requestData[$name])) {
// Converting from array to int is "undefined". Don't rely on whatever
// PHP decides to do.
if (is_array($this->requestData[$name])) {
return $default;
}
return (int)$this->requestData[$name];
} else {
return $default;
}
}
/**
* @task data
*/
public function getBool($name, $default = null) {
if (isset($this->requestData[$name])) {
if ($this->requestData[$name] === 'true') {
return true;
} else if ($this->requestData[$name] === 'false') {
return false;
} else {
return (bool)$this->requestData[$name];
}
} else {
return $default;
}
}
/**
* @task data
*/
public function getStr($name, $default = null) {
if (isset($this->requestData[$name])) {
$str = (string)$this->requestData[$name];
// Normalize newline craziness.
$str = str_replace(
array("\r\n", "\r"),
array("\n", "\n"),
$str);
return $str;
} else {
return $default;
}
}
/**
* @task data
*/
public function getArr($name, $default = array()) {
if (isset($this->requestData[$name]) &&
is_array($this->requestData[$name])) {
return $this->requestData[$name];
} else {
return $default;
}
}
/**
* @task data
*/
public function getStrList($name, $default = array()) {
if (!isset($this->requestData[$name])) {
return $default;
}
$list = $this->getStr($name);
$list = preg_split('/[\s,]+/', $list, $limit = -1, PREG_SPLIT_NO_EMPTY);
return $list;
}
/**
* @task data
*/
public function getExists($name) {
return array_key_exists($name, $this->requestData);
}
public function getFileExists($name) {
return isset($_FILES[$name]) &&
(idx($_FILES[$name], 'error') !== UPLOAD_ERR_NO_FILE);
}
public function isHTTPGet() {
return ($_SERVER['REQUEST_METHOD'] == 'GET');
}
public function isHTTPPost() {
return ($_SERVER['REQUEST_METHOD'] == 'POST');
}
public function isAjax() {
return $this->getExists(self::TYPE_AJAX) && !$this->isQuicksand();
}
public function isWorkflow() {
return $this->getExists(self::TYPE_WORKFLOW) && !$this->isQuicksand();
}
public function isQuicksand() {
return $this->getExists(self::TYPE_QUICKSAND);
}
public function isConduit() {
return $this->getExists(self::TYPE_CONDUIT);
}
public static function getCSRFTokenName() {
return '__csrf__';
}
public static function getCSRFHeaderName() {
return 'X-Phabricator-Csrf';
}
public static function getViaHeaderName() {
return 'X-Phabricator-Via';
}
public function validateCSRF() {
$token_name = self::getCSRFTokenName();
$token = $this->getStr($token_name);
// No token in the request, check the HTTP header which is added for Ajax
// requests.
if (empty($token)) {
$token = self::getHTTPHeader(self::getCSRFHeaderName());
}
$valid = $this->getUser()->validateCSRFToken($token);
if (!$valid) {
// Add some diagnostic details so we can figure out if some CSRF issues
// are JS problems or people accessing Ajax URIs directly with their
// browsers.
$info = array();
$info[] = pht(
'You are trying to save some data to Phabricator, but the request '.
'your browser made included an incorrect token. Reload the page '.
'and try again. You may need to clear your cookies.');
if ($this->isAjax()) {
$info[] = pht('This was an Ajax request.');
} else {
$info[] = pht('This was a Web request.');
}
if ($token) {
$info[] = pht('This request had an invalid CSRF token.');
} else {
$info[] = pht('This request had no CSRF token.');
}
// Give a more detailed explanation of how to avoid the exception
// in developer mode.
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
// TODO: Clean this up, see T1921.
$info[] = pht(
"To avoid this error, use %s to construct forms. If you are already ".
"using %s, make sure the form 'action' uses a relative URI (i.e., ".
"begins with a '%s'). Forms using absolute URIs do not include CSRF ".
"tokens, to prevent leaking tokens to external sites.\n\n".
"If this page performs writes which do not require CSRF protection ".
"(usually, filling caches or logging), you can use %s to ".
"temporarily bypass CSRF protection while writing. You should use ".
"this only for writes which can not be protected with normal CSRF ".
"mechanisms.\n\n".
"Some UI elements (like %s) also have methods which will allow you ".
"to render links as forms (like %s).",
'phabricator_form()',
'phabricator_form()',
'/',
'AphrontWriteGuard::beginScopedUnguardedWrites()',
'PhabricatorActionListView',
'setRenderAsForm(true)');
}
$message = implode("\n", $info);
// This should only be able to happen if you load a form, pull your
// internet for 6 hours, and then reconnect and immediately submit,
// but give the user some indication of what happened since the workflow
// is incredibly confusing otherwise.
throw new AphrontMalformedRequestException(
pht('Invalid Request (CSRF)'),
$message,
true);
}
return true;
}
public function isFormPost() {
$post = $this->getExists(self::TYPE_FORM) &&
!$this->getExists(self::TYPE_HISEC) &&
$this->isHTTPPost();
if (!$post) {
return false;
}
return $this->validateCSRF();
}
public function isFormOrHisecPost() {
$post = $this->getExists(self::TYPE_FORM) &&
$this->isHTTPPost();
if (!$post) {
return false;
}
return $this->validateCSRF();
}
public function setCookiePrefix($prefix) {
$this->cookiePrefix = $prefix;
return $this;
}
private function getPrefixedCookieName($name) {
if (strlen($this->cookiePrefix)) {
return $this->cookiePrefix.'_'.$name;
} else {
return $name;
}
}
public function getCookie($name, $default = null) {
$name = $this->getPrefixedCookieName($name);
$value = idx($_COOKIE, $name, $default);
// Internally, PHP deletes cookies by setting them to the value 'deleted'
// with an expiration date in the past.
// At least in Safari, the browser may send this cookie anyway in some
// circumstances. After logging out, the 302'd GET to /login/ consistently
// includes deleted cookies on my local install. If a cookie value is
// literally 'deleted', pretend it does not exist.
if ($value === 'deleted') {
return null;
}
return $value;
}
public function clearCookie($name) {
$this->setCookieWithExpiration($name, '', time() - (60 * 60 * 24 * 30));
unset($_COOKIE[$name]);
}
/**
* Get the domain which cookies should be set on for this request, or null
* if the request does not correspond to a valid cookie domain.
*
* @return PhutilURI|null Domain URI, or null if no valid domain exists.
*
* @task cookie
*/
private function getCookieDomainURI() {
if (PhabricatorEnv::getEnvConfig('security.require-https') &&
!$this->isHTTPS()) {
return null;
}
$host = $this->getHost();
// If there's no base domain configured, just use whatever the request
// domain is. This makes setup easier, and we'll tell administrators to
// configure a base domain during the setup process.
$base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
if (!strlen($base_uri)) {
return new PhutilURI('http://'.$host.'/');
}
$alternates = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris');
$allowed_uris = array_merge(
array($base_uri),
$alternates);
foreach ($allowed_uris as $allowed_uri) {
$uri = new PhutilURI($allowed_uri);
if ($uri->getDomain() == $host) {
return $uri;
}
}
return null;
}
/**
* Determine if security policy rules will allow cookies to be set when
* responding to the request.
*
* @return bool True if setCookie() will succeed. If this method returns
* false, setCookie() will throw.
*
* @task cookie
*/
public function canSetCookies() {
return (bool)$this->getCookieDomainURI();
}
/**
* Set a cookie which does not expire for a long time.
*
* To set a temporary cookie, see @{method:setTemporaryCookie}.
*
* @param string Cookie name.
* @param string Cookie value.
* @return this
* @task cookie
*/
public function setCookie($name, $value) {
$far_future = time() + (60 * 60 * 24 * 365 * 5);
return $this->setCookieWithExpiration($name, $value, $far_future);
}
/**
* Set a cookie which expires soon.
*
* To set a durable cookie, see @{method:setCookie}.
*
* @param string Cookie name.
* @param string Cookie value.
* @return this
* @task cookie
*/
public function setTemporaryCookie($name, $value) {
return $this->setCookieWithExpiration($name, $value, 0);
}
/**
* Set a cookie with a given expiration policy.
*
* @param string Cookie name.
* @param string Cookie value.
* @param int Epoch timestamp for cookie expiration.
* @return this
* @task cookie
*/
private function setCookieWithExpiration(
$name,
$value,
$expire) {
$is_secure = false;
$base_domain_uri = $this->getCookieDomainURI();
if (!$base_domain_uri) {
$configured_as = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
$accessed_as = $this->getHost();
throw new AphrontMalformedRequestException(
pht('Bad Host Header'),
pht(
'This Phabricator install is configured as "%s", but you are '.
'using the domain name "%s" to access a page which is trying to '.
'set a cookie. Access Phabricator on the configured primary '.
'domain or a configured alternate domain. Phabricator will not '.
'set cookies on other domains for security reasons.',
$configured_as,
$accessed_as),
true);
}
$base_domain = $base_domain_uri->getDomain();
$is_secure = ($base_domain_uri->getProtocol() == 'https');
$name = $this->getPrefixedCookieName($name);
if (php_sapi_name() == 'cli') {
// Do nothing, to avoid triggering "Cannot modify header information"
// warnings.
// TODO: This is effectively a test for whether we're running in a unit
// test or not. Move this actual call to HTTPSink?
} else {
setcookie(
$name,
$value,
$expire,
$path = '/',
$base_domain,
$is_secure,
$http_only = true);
}
$_COOKIE[$name] = $value;
return $this;
}
public function setUser($user) {
$this->user = $user;
return $this;
}
public function getUser() {
return $this->user;
}
public function getViewer() {
return $this->user;
}
public function getRequestURI() {
- $get = $_GET;
- unset($get['__path__']);
- $path = phutil_escape_uri($this->getPath());
- return id(new PhutilURI($path))->setQueryParams($get);
+ $uri_path = phutil_escape_uri($this->getPath());
+ $uri_query = idx($_SERVER, 'QUERY_STRING', '');
+
+ return id(new PhutilURI($uri_path.'?'.$uri_query))
+ ->removeQueryParam('__path__');
}
public function getAbsoluteRequestURI() {
$uri = $this->getRequestURI();
$uri->setDomain($this->getHost());
if ($this->isHTTPS()) {
$protocol = 'https';
} else {
$protocol = 'http';
}
$uri->setProtocol($protocol);
// If the request used a nonstandard port, preserve it while building the
// absolute URI.
// First, get the default port for the request protocol.
$default_port = id(new PhutilURI($protocol.'://example.com/'))
->getPortWithProtocolDefault();
// NOTE: See note in getHost() about malicious "Host" headers. This
// construction defuses some obscure potential attacks.
$port = id(new PhutilURI($protocol.'://'.$this->host))
->getPort();
if (($port !== null) && ($port !== $default_port)) {
$uri->setPort($port);
}
return $uri;
}
public function isDialogFormPost() {
return $this->isFormPost() && $this->getStr('__dialog__');
}
public function getRemoteAddress() {
$address = PhabricatorEnv::getRemoteAddress();
if (!$address) {
return null;
}
return $address->getAddress();
}
public function isHTTPS() {
if (empty($_SERVER['HTTPS'])) {
return false;
}
if (!strcasecmp($_SERVER['HTTPS'], 'off')) {
return false;
}
return true;
}
public function isContinueRequest() {
return $this->isFormPost() && $this->getStr('__continue__');
}
public function isPreviewRequest() {
return $this->isFormPost() && $this->getStr('__preview__');
}
/**
* Get application request parameters in a flattened form suitable for
* inclusion in an HTTP request, excluding parameters with special meanings.
* This is primarily useful if you want to ask the user for more input and
* then resubmit their request.
*
* @return dict<string, string> Original request parameters.
*/
public function getPassthroughRequestParameters($include_quicksand = false) {
return self::flattenData(
$this->getPassthroughRequestData($include_quicksand));
}
/**
* Get request data other than "magic" parameters.
*
* @return dict<string, wild> Request data, with magic filtered out.
*/
public function getPassthroughRequestData($include_quicksand = false) {
$data = $this->getRequestData();
// Remove magic parameters like __dialog__ and __ajax__.
foreach ($data as $key => $value) {
if ($include_quicksand && $key == self::TYPE_QUICKSAND) {
continue;
}
if (!strncmp($key, '__', 2)) {
unset($data[$key]);
}
}
return $data;
}
/**
* Flatten an array of key-value pairs (possibly including arrays as values)
* into a list of key-value pairs suitable for submitting via HTTP request
* (with arrays flattened).
*
* @param dict<string, wild> Data to flatten.
* @return dict<string, string> Flat data suitable for inclusion in an HTTP
* request.
*/
public static function flattenData(array $data) {
$result = array();
foreach ($data as $key => $value) {
if (is_array($value)) {
foreach (self::flattenData($value) as $fkey => $fvalue) {
$fkey = '['.preg_replace('/(?=\[)|$/', ']', $fkey, $limit = 1);
$result[$key.$fkey] = $fvalue;
}
} else {
$result[$key] = (string)$value;
}
}
ksort($result);
return $result;
}
/**
* Read the value of an HTTP header from `$_SERVER`, or a similar datasource.
*
* This function accepts a canonical header name, like `"Accept-Encoding"`,
* and looks up the appropriate value in `$_SERVER` (in this case,
* `"HTTP_ACCEPT_ENCODING"`).
*
* @param string Canonical header name, like `"Accept-Encoding"`.
* @param wild Default value to return if header is not present.
* @param array? Read this instead of `$_SERVER`.
* @return string|wild Header value if present, or `$default` if not.
*/
public static function getHTTPHeader($name, $default = null, $data = null) {
// PHP mangles HTTP headers by uppercasing them and replacing hyphens with
// underscores, then prepending 'HTTP_'.
$php_index = strtoupper($name);
$php_index = str_replace('-', '_', $php_index);
$try_names = array();
$try_names[] = 'HTTP_'.$php_index;
if ($php_index == 'CONTENT_TYPE' || $php_index == 'CONTENT_LENGTH') {
// These headers may be available under alternate names. See
// http://www.php.net/manual/en/reserved.variables.server.php#110763
$try_names[] = $php_index;
}
if ($data === null) {
$data = $_SERVER;
}
foreach ($try_names as $try_name) {
if (array_key_exists($try_name, $data)) {
return $data[$try_name];
}
}
return $default;
}
/* -( Working With a Phabricator Cluster )--------------------------------- */
/**
* Is this a proxied request originating from within the Phabricator cluster?
*
* IMPORTANT: This means the request is dangerous!
*
* These requests are **more dangerous** than normal requests (they can not
* be safely proxied, because proxying them may cause a loop). Cluster
* requests are not guaranteed to come from a trusted source, and should
* never be treated as safer than normal requests. They are strictly less
* safe.
*/
public function isProxiedClusterRequest() {
return (bool)self::getHTTPHeader('X-Phabricator-Cluster');
}
/**
* Build a new @{class:HTTPSFuture} which proxies this request to another
* node in the cluster.
*
* IMPORTANT: This is very dangerous!
*
* The future forwards authentication information present in the request.
* Proxied requests must only be sent to trusted hosts. (We attempt to
* enforce this.)
*
* This is not a general-purpose proxying method; it is a specialized
* method with niche applications and severe security implications.
*
* @param string URI identifying the host we are proxying the request to.
* @return HTTPSFuture New proxy future.
*
* @phutil-external-symbol class PhabricatorStartup
*/
public function newClusterProxyFuture($uri) {
$uri = new PhutilURI($uri);
$domain = $uri->getDomain();
$ip = gethostbyname($domain);
if (!$ip) {
throw new Exception(
pht(
'Unable to resolve domain "%s"!',
$domain));
}
if (!PhabricatorEnv::isClusterAddress($ip)) {
throw new Exception(
pht(
'Refusing to proxy a request to IP address ("%s") which is not '.
'in the cluster address block (this address was derived by '.
'resolving the domain "%s").',
$ip,
$domain));
}
$uri->setPath($this->getPath());
- $uri->setQueryParams(self::flattenData($_GET));
+ $uri->removeAllQueryParams();
+ foreach (self::flattenData($_GET) as $query_key => $query_value) {
+ $uri->appendQueryParam($query_key, $query_value);
+ }
$input = PhabricatorStartup::getRawInput();
$future = id(new HTTPSFuture($uri))
->addHeader('Host', self::getHost())
->addHeader('X-Phabricator-Cluster', true)
->setMethod($_SERVER['REQUEST_METHOD'])
->write($input);
if (isset($_SERVER['PHP_AUTH_USER'])) {
$future->setHTTPBasicAuthCredentials(
$_SERVER['PHP_AUTH_USER'],
new PhutilOpaqueEnvelope(idx($_SERVER, 'PHP_AUTH_PW', '')));
}
$headers = array();
$seen = array();
// NOTE: apache_request_headers() might provide a nicer way to do this,
// but isn't available under FCGI until PHP 5.4.0.
foreach ($_SERVER as $key => $value) {
if (!preg_match('/^HTTP_/', $key)) {
continue;
}
// Unmangle the header as best we can.
$key = substr($key, strlen('HTTP_'));
$key = str_replace('_', ' ', $key);
$key = strtolower($key);
$key = ucwords($key);
$key = str_replace(' ', '-', $key);
// By default, do not forward headers.
$should_forward = false;
// Forward "X-Hgarg-..." headers.
if (preg_match('/^X-Hgarg-/', $key)) {
$should_forward = true;
}
if ($should_forward) {
$headers[] = array($key, $value);
$seen[$key] = true;
}
}
// In some situations, this may not be mapped into the HTTP_X constants.
// CONTENT_LENGTH is similarly affected, but we trust cURL to take care
// of that if it matters, since we're handing off a request body.
if (empty($seen['Content-Type'])) {
if (isset($_SERVER['CONTENT_TYPE'])) {
$headers[] = array('Content-Type', $_SERVER['CONTENT_TYPE']);
}
}
foreach ($headers as $header) {
list($key, $value) = $header;
switch ($key) {
case 'Host':
case 'Authorization':
// Don't forward these headers, we've already handled them elsewhere.
unset($headers[$key]);
break;
default:
break;
}
}
foreach ($headers as $header) {
list($key, $value) = $header;
$future->addHeader($key, $value);
}
return $future;
}
}
diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php
index 8d36bbc88..a47920912 100644
--- a/src/aphront/configuration/AphrontApplicationConfiguration.php
+++ b/src/aphront/configuration/AphrontApplicationConfiguration.php
@@ -1,817 +1,868 @@
<?php
/**
* @task routing URI Routing
* @task response Response Handling
* @task exception Exception Handling
*/
final class AphrontApplicationConfiguration
extends Phobject {
private $request;
private $host;
private $path;
private $console;
public function buildRequest() {
$parser = new PhutilQueryStringParser();
$data = array();
$data += $_POST;
$data += $parser->parseQueryString(idx($_SERVER, 'QUERY_STRING', ''));
$cookie_prefix = PhabricatorEnv::getEnvConfig('phabricator.cookie-prefix');
$request = new AphrontRequest($this->getHost(), $this->getPath());
$request->setRequestData($data);
$request->setApplicationConfiguration($this);
$request->setCookiePrefix($cookie_prefix);
return $request;
}
public function build404Controller() {
return array(new Phabricator404Controller(), array());
}
public function buildRedirectController($uri, $external) {
return array(
new PhabricatorRedirectController(),
array(
'uri' => $uri,
'external' => $external,
),
);
}
public function setRequest(AphrontRequest $request) {
$this->request = $request;
return $this;
}
public function getRequest() {
return $this->request;
}
public function getConsole() {
return $this->console;
}
public function setConsole($console) {
$this->console = $console;
return $this;
}
public function setHost($host) {
$this->host = $host;
return $this;
}
public function getHost() {
return $this->host;
}
public function setPath($path) {
$this->path = $path;
return $this;
}
public function getPath() {
return $this->path;
}
/**
* @phutil-external-symbol class PhabricatorStartup
*/
public static function runHTTPRequest(AphrontHTTPSink $sink) {
if (isset($_SERVER['HTTP_X_PHABRICATOR_SELFCHECK'])) {
$response = self::newSelfCheckResponse();
return self::writeResponse($sink, $response);
}
PhabricatorStartup::beginStartupPhase('multimeter');
$multimeter = MultimeterControl::newInstance();
$multimeter->setEventContext('<http-init>');
$multimeter->setEventViewer('<none>');
// Build a no-op write guard for the setup phase. We'll replace this with a
// real write guard later on, but we need to survive setup and build a
// request object first.
$write_guard = new AphrontWriteGuard('id');
PhabricatorStartup::beginStartupPhase('preflight');
$response = PhabricatorSetupCheck::willPreflightRequest();
if ($response) {
return self::writeResponse($sink, $response);
}
PhabricatorStartup::beginStartupPhase('env.init');
self::readHTTPPOSTData();
try {
PhabricatorEnv::initializeWebEnvironment();
$database_exception = null;
} catch (PhabricatorClusterStrandedException $ex) {
$database_exception = $ex;
}
+ // If we're in developer mode, set a flag so that top-level exception
+ // handlers can add more information.
+ if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
+ $sink->setShowStackTraces(true);
+ }
+
if ($database_exception) {
$issue = PhabricatorSetupIssue::newDatabaseConnectionIssue(
$database_exception,
true);
$response = PhabricatorSetupCheck::newIssueResponse($issue);
return self::writeResponse($sink, $response);
}
$multimeter->setSampleRate(
PhabricatorEnv::getEnvConfig('debug.sample-rate'));
$debug_time_limit = PhabricatorEnv::getEnvConfig('debug.time-limit');
if ($debug_time_limit) {
PhabricatorStartup::setDebugTimeLimit($debug_time_limit);
}
// This is the earliest we can get away with this, we need env config first.
PhabricatorStartup::beginStartupPhase('log.access');
PhabricatorAccessLog::init();
$access_log = PhabricatorAccessLog::getLog();
PhabricatorStartup::setAccessLog($access_log);
$address = PhabricatorEnv::getRemoteAddress();
if ($address) {
$address_string = $address->getAddress();
} else {
$address_string = '-';
}
$access_log->setData(
array(
'R' => AphrontRequest::getHTTPHeader('Referer', '-'),
'r' => $address_string,
'M' => idx($_SERVER, 'REQUEST_METHOD', '-'),
));
DarkConsoleXHProfPluginAPI::hookProfiler();
// We just activated the profiler, so we don't need to keep track of
// startup phases anymore: it can take over from here.
PhabricatorStartup::beginStartupPhase('startup.done');
DarkConsoleErrorLogPluginAPI::registerErrorHandler();
$response = PhabricatorSetupCheck::willProcessRequest();
if ($response) {
return self::writeResponse($sink, $response);
}
$host = AphrontRequest::getHTTPHeader('Host');
$path = $_REQUEST['__path__'];
$application = new self();
$application->setHost($host);
$application->setPath($path);
$request = $application->buildRequest();
// Now that we have a request, convert the write guard into one which
// actually checks CSRF tokens.
$write_guard->dispose();
$write_guard = new AphrontWriteGuard(array($request, 'validateCSRF'));
// Build the server URI implied by the request headers. If an administrator
// has not configured "phabricator.base-uri" yet, we'll use this to generate
// links.
$request_protocol = ($request->isHTTPS() ? 'https' : 'http');
$request_base_uri = "{$request_protocol}://{$host}/";
PhabricatorEnv::setRequestBaseURI($request_base_uri);
$access_log->setData(
array(
'U' => (string)$request->getRequestURI()->getPath(),
));
$processing_exception = null;
try {
$response = $application->processRequest(
$request,
$access_log,
$sink,
$multimeter);
$response_code = $response->getHTTPResponseCode();
} catch (Exception $ex) {
$processing_exception = $ex;
$response_code = 500;
}
$write_guard->dispose();
$access_log->setData(
array(
'c' => $response_code,
'T' => PhabricatorStartup::getMicrosecondsSinceStart(),
));
$multimeter->newEvent(
MultimeterEvent::TYPE_REQUEST_TIME,
$multimeter->getEventContext(),
PhabricatorStartup::getMicrosecondsSinceStart());
$access_log->write();
$multimeter->saveEvents();
DarkConsoleXHProfPluginAPI::saveProfilerSample($access_log);
PhabricatorStartup::disconnectRateLimits(
array(
'viewer' => $request->getUser(),
));
if ($processing_exception) {
throw $processing_exception;
}
}
public function processRequest(
AphrontRequest $request,
PhutilDeferredLog $access_log,
AphrontHTTPSink $sink,
MultimeterControl $multimeter) {
$this->setRequest($request);
list($controller, $uri_data) = $this->buildController();
$controller_class = get_class($controller);
$access_log->setData(
array(
'C' => $controller_class,
));
$multimeter->setEventContext('web.'.$controller_class);
$request->setController($controller);
$request->setURIMap($uri_data);
$controller->setRequest($request);
// If execution throws an exception and then trying to render that
// exception throws another exception, we want to show the original
// exception, as it is likely the root cause of the rendering exception.
$original_exception = null;
try {
$response = $controller->willBeginExecution();
if ($request->getUser() && $request->getUser()->getPHID()) {
$access_log->setData(
array(
'u' => $request->getUser()->getUserName(),
'P' => $request->getUser()->getPHID(),
));
$multimeter->setEventViewer('user.'.$request->getUser()->getPHID());
}
if (!$response) {
$controller->willProcessRequest($uri_data);
$response = $controller->handleRequest($request);
$this->validateControllerResponse($controller, $response);
}
} catch (Exception $ex) {
$original_exception = $ex;
- $response = $this->handleThrowable($ex);
} catch (Throwable $ex) {
$original_exception = $ex;
- $response = $this->handleThrowable($ex);
}
+ $response_exception = null;
try {
+ if ($original_exception) {
+ $response = $this->handleThrowable($original_exception);
+ }
+
$response = $this->produceResponse($request, $response);
$response = $controller->willSendResponse($response);
$response->setRequest($request);
self::writeResponse($sink, $response);
} catch (Exception $ex) {
+ $response_exception = $ex;
+ } catch (Throwable $ex) {
+ $response_exception = $ex;
+ }
+
+ if ($response_exception) {
+ // If we encountered an exception while building a normal response, then
+ // encountered another exception while building a response for the first
+ // exception, just throw the original exception. It is more likely to be
+ // useful and point at a root cause than the second exception we ran into
+ // while telling the user about it.
if ($original_exception) {
throw $original_exception;
}
- throw $ex;
+
+ // If we built a response successfully and then ran into an exception
+ // trying to render it, try to handle and present that exception to the
+ // user using the standard handler.
+
+ // The problem here might be in rendering (more common) or in the actual
+ // response mechanism (less common). If it's in rendering, we can likely
+ // still render a nice exception page: the majority of rendering issues
+ // are in main page content, not content shared with the exception page.
+
+ $handling_exception = null;
+ try {
+ $response = $this->handleThrowable($response_exception);
+
+ $response = $this->produceResponse($request, $response);
+ $response = $controller->willSendResponse($response);
+ $response->setRequest($request);
+
+ self::writeResponse($sink, $response);
+ } catch (Exception $ex) {
+ $handling_exception = $ex;
+ } catch (Throwable $ex) {
+ $handling_exception = $ex;
+ }
+
+ // If we didn't have any luck with that, raise the original response
+ // exception. As above, this is the root cause exception and more likely
+ // to be useful. This will go to the fallback error handler at top
+ // level.
+
+ if ($handling_exception) {
+ throw $response_exception;
+ }
}
return $response;
}
private static function writeResponse(
AphrontHTTPSink $sink,
AphrontResponse $response) {
$unexpected_output = PhabricatorStartup::endOutputCapture();
if ($unexpected_output) {
$unexpected_output = pht(
"Unexpected output:\n\n%s",
$unexpected_output);
phlog($unexpected_output);
if ($response instanceof AphrontWebpageResponse) {
$response->setUnexpectedOutput($unexpected_output);
}
}
$sink->writeResponse($response);
}
/* -( URI Routing )-------------------------------------------------------- */
/**
* Build a controller to respond to the request.
*
* @return pair<AphrontController,dict> Controller and dictionary of request
* parameters.
* @task routing
*/
private function buildController() {
$request = $this->getRequest();
// If we're configured to operate in cluster mode, reject requests which
// were not received on a cluster interface.
//
// For example, a host may have an internal address like "170.0.0.1", and
// also have a public address like "51.23.95.16". Assuming the cluster
// is configured on a range like "170.0.0.0/16", we want to reject the
// requests received on the public interface.
//
// Ideally, nodes in a cluster should only be listening on internal
// interfaces, but they may be configured in such a way that they also
// listen on external interfaces, since this is easy to forget about or
// get wrong. As a broad security measure, reject requests received on any
// interfaces which aren't on the whitelist.
$cluster_addresses = PhabricatorEnv::getEnvConfig('cluster.addresses');
if ($cluster_addresses) {
$server_addr = idx($_SERVER, 'SERVER_ADDR');
if (!$server_addr) {
if (php_sapi_name() == 'cli') {
// This is a command line script (probably something like a unit
// test) so it's fine that we don't have SERVER_ADDR defined.
} else {
throw new AphrontMalformedRequestException(
pht('No %s', 'SERVER_ADDR'),
pht(
'Phabricator is configured to operate in cluster mode, but '.
'%s is not defined in the request context. Your webserver '.
'configuration needs to forward %s to PHP so Phabricator can '.
'reject requests received on external interfaces.',
'SERVER_ADDR',
'SERVER_ADDR'));
}
} else {
if (!PhabricatorEnv::isClusterAddress($server_addr)) {
throw new AphrontMalformedRequestException(
pht('External Interface'),
pht(
'Phabricator is configured in cluster mode and the address '.
'this request was received on ("%s") is not whitelisted as '.
'a cluster address.',
$server_addr));
}
}
}
$site = $this->buildSiteForRequest($request);
if ($site->shouldRequireHTTPS()) {
if (!$request->isHTTPS()) {
// Don't redirect intracluster requests: doing so drops headers and
// parameters, imposes a performance penalty, and indicates a
// misconfiguration.
if ($request->isProxiedClusterRequest()) {
throw new AphrontMalformedRequestException(
pht('HTTPS Required'),
pht(
'This request reached a site which requires HTTPS, but the '.
'request is not marked as HTTPS.'));
}
$https_uri = $request->getRequestURI();
$https_uri->setDomain($request->getHost());
$https_uri->setProtocol('https');
// In this scenario, we'll be redirecting to HTTPS using an absolute
// URI, so we need to permit an external redirect.
return $this->buildRedirectController($https_uri, true);
}
}
$maps = $site->getRoutingMaps();
$path = $request->getPath();
$result = $this->routePath($maps, $path);
if ($result) {
return $result;
}
// If we failed to match anything but don't have a trailing slash, try
// to add a trailing slash and issue a redirect if that resolves.
// NOTE: We only do this for GET, since redirects switch to GET and drop
// data like POST parameters.
if (!preg_match('@/$@', $path) && $request->isHTTPGet()) {
$result = $this->routePath($maps, $path.'/');
if ($result) {
$target_uri = $request->getAbsoluteRequestURI();
// We need to restore URI encoding because the webserver has
// interpreted it. For example, this allows us to redirect a path
// like `/tag/aa%20bb` to `/tag/aa%20bb/`, which may eventually be
// resolved meaningfully by an application.
$target_path = phutil_escape_uri($path.'/');
$target_uri->setPath($target_path);
$target_uri = (string)$target_uri;
return $this->buildRedirectController($target_uri, true);
}
}
$result = $site->new404Controller($request);
if ($result) {
return array($result, array());
}
return $this->build404Controller();
}
/**
* Map a specific path to the corresponding controller. For a description
* of routing, see @{method:buildController}.
*
* @param list<AphrontRoutingMap> List of routing maps.
* @param string Path to route.
* @return pair<AphrontController,dict> Controller and dictionary of request
* parameters.
* @task routing
*/
private function routePath(array $maps, $path) {
foreach ($maps as $map) {
$result = $map->routePath($path);
if ($result) {
return array($result->getController(), $result->getURIData());
}
}
}
private function buildSiteForRequest(AphrontRequest $request) {
$sites = PhabricatorSite::getAllSites();
$site = null;
foreach ($sites as $candidate) {
$site = $candidate->newSiteForRequest($request);
if ($site) {
break;
}
}
if (!$site) {
$path = $request->getPath();
$host = $request->getHost();
throw new AphrontMalformedRequestException(
pht('Site Not Found'),
pht(
'This request asked for "%s" on host "%s", but no site is '.
'configured which can serve this request.',
$path,
$host),
true);
}
$request->setSite($site);
return $site;
}
/* -( Response Handling )-------------------------------------------------- */
/**
* Tests if a response is of a valid type.
*
* @param wild Supposedly valid response.
* @return bool True if the object is of a valid type.
* @task response
*/
private function isValidResponseObject($response) {
if ($response instanceof AphrontResponse) {
return true;
}
if ($response instanceof AphrontResponseProducerInterface) {
return true;
}
return false;
}
/**
* Verifies that the return value from an @{class:AphrontController} is
* of an allowed type.
*
* @param AphrontController Controller which returned the response.
* @param wild Supposedly valid response.
* @return void
* @task response
*/
private function validateControllerResponse(
AphrontController $controller,
$response) {
if ($this->isValidResponseObject($response)) {
return;
}
throw new Exception(
pht(
'Controller "%s" returned an invalid response from call to "%s". '.
'This method must return an object of class "%s", or an object '.
'which implements the "%s" interface.',
get_class($controller),
'handleRequest()',
'AphrontResponse',
'AphrontResponseProducerInterface'));
}
/**
* Verifies that the return value from an
* @{class:AphrontResponseProducerInterface} is of an allowed type.
*
* @param AphrontResponseProducerInterface Object which produced
* this response.
* @param wild Supposedly valid response.
* @return void
* @task response
*/
private function validateProducerResponse(
AphrontResponseProducerInterface $producer,
$response) {
if ($this->isValidResponseObject($response)) {
return;
}
throw new Exception(
pht(
'Producer "%s" returned an invalid response from call to "%s". '.
'This method must return an object of class "%s", or an object '.
'which implements the "%s" interface.',
get_class($producer),
'produceAphrontResponse()',
'AphrontResponse',
'AphrontResponseProducerInterface'));
}
/**
* Verifies that the return value from an
* @{class:AphrontRequestExceptionHandler} is of an allowed type.
*
* @param AphrontRequestExceptionHandler Object which produced this
* response.
* @param wild Supposedly valid response.
* @return void
* @task response
*/
private function validateErrorHandlerResponse(
AphrontRequestExceptionHandler $handler,
$response) {
if ($this->isValidResponseObject($response)) {
return;
}
throw new Exception(
pht(
'Exception handler "%s" returned an invalid response from call to '.
'"%s". This method must return an object of class "%s", or an object '.
'which implements the "%s" interface.',
get_class($handler),
'handleRequestException()',
'AphrontResponse',
'AphrontResponseProducerInterface'));
}
/**
* Resolves a response object into an @{class:AphrontResponse}.
*
* Controllers are permitted to return actual responses of class
* @{class:AphrontResponse}, or other objects which implement
* @{interface:AphrontResponseProducerInterface} and can produce a response.
*
* If a controller returns a response producer, invoke it now and produce
* the real response.
*
* @param AphrontRequest Request being handled.
* @param AphrontResponse|AphrontResponseProducerInterface Response, or
* response producer.
* @return AphrontResponse Response after any required production.
* @task response
*/
private function produceResponse(AphrontRequest $request, $response) {
$original = $response;
// Detect cycles on the exact same objects. It's still possible to produce
// infinite responses as long as they're all unique, but we can only
// reasonably detect cycles, not guarantee that response production halts.
$seen = array();
while (true) {
// NOTE: It is permissible for an object to be both a response and a
// response producer. If so, being a producer is "stronger". This is
// used by AphrontProxyResponse.
// If this response is a valid response, hand over the request first.
if ($response instanceof AphrontResponse) {
$response->setRequest($request);
}
// If this isn't a producer, we're all done.
if (!($response instanceof AphrontResponseProducerInterface)) {
break;
}
$hash = spl_object_hash($response);
if (isset($seen[$hash])) {
throw new Exception(
pht(
'Failure while producing response for object of class "%s": '.
'encountered production cycle (identical object, of class "%s", '.
'was produced twice).',
get_class($original),
get_class($response)));
}
$seen[$hash] = true;
$new_response = $response->produceAphrontResponse();
$this->validateProducerResponse($response, $new_response);
$response = $new_response;
}
return $response;
}
/* -( Error Handling )----------------------------------------------------- */
/**
* Convert an exception which has escaped the controller into a response.
*
* This method delegates exception handling to available subclasses of
* @{class:AphrontRequestExceptionHandler}.
*
* @param Throwable Exception which needs to be handled.
* @return wild Response or response producer, or null if no available
* handler can produce a response.
* @task exception
*/
private function handleThrowable($throwable) {
$handlers = AphrontRequestExceptionHandler::getAllHandlers();
$request = $this->getRequest();
foreach ($handlers as $handler) {
if ($handler->canHandleRequestThrowable($request, $throwable)) {
$response = $handler->handleRequestThrowable($request, $throwable);
$this->validateErrorHandlerResponse($handler, $response);
return $response;
}
}
throw $throwable;
}
private static function newSelfCheckResponse() {
$path = idx($_REQUEST, '__path__', '');
$query = idx($_SERVER, 'QUERY_STRING', '');
$pairs = id(new PhutilQueryStringParser())
->parseQueryStringToPairList($query);
$params = array();
foreach ($pairs as $v) {
$params[] = array(
'name' => $v[0],
'value' => $v[1],
);
}
$result = array(
'path' => $path,
'params' => $params,
'user' => idx($_SERVER, 'PHP_AUTH_USER'),
'pass' => idx($_SERVER, 'PHP_AUTH_PW'),
// This just makes sure that the response compresses well, so reasonable
// algorithms should want to gzip or deflate it.
'filler' => str_repeat('Q', 1024 * 16),
);
-
return id(new AphrontJSONResponse())
->setAddJSONShield(false)
->setContent($result);
}
private static function readHTTPPOSTData() {
$request_method = idx($_SERVER, 'REQUEST_METHOD');
if ($request_method === 'PUT') {
// For PUT requests, do nothing: in particular, do NOT read input. This
// allows us to stream input later and process very large PUT requests,
// like those coming from Git LFS.
return;
}
// For POST requests, we're going to read the raw input ourselves here
// if we can. Among other things, this corrects variable names with
// the "." character in them, which PHP normally converts into "_".
// There are two major considerations here: whether the
// `enable_post_data_reading` option is set, and whether the content
// type is "multipart/form-data" or not.
// If `enable_post_data_reading` is off, we're free to read the entire
// raw request body and parse it -- and we must, because $_POST and
// $_FILES are not built for us. If `enable_post_data_reading` is on,
// which is the default, we may not be able to read the body (the
// documentation says we can't, but empirically we can at least some
// of the time).
// If the content type is "multipart/form-data", we need to build both
// $_POST and $_FILES, which is involved. The body itself is also more
// difficult to parse than other requests.
$raw_input = PhabricatorStartup::getRawInput();
$parser = new PhutilQueryStringParser();
if (strlen($raw_input)) {
$content_type = idx($_SERVER, 'CONTENT_TYPE');
$is_multipart = preg_match('@^multipart/form-data@i', $content_type);
if ($is_multipart && !ini_get('enable_post_data_reading')) {
$multipart_parser = id(new AphrontMultipartParser())
->setContentType($content_type);
$multipart_parser->beginParse();
$multipart_parser->continueParse($raw_input);
$parts = $multipart_parser->endParse();
// We're building and then parsing a query string so that requests
// with arrays (like "x[]=apple&x[]=banana") work correctly. This also
// means we can't use "phutil_build_http_querystring()", since it
// can't build a query string with duplicate names.
$query_string = array();
foreach ($parts as $part) {
if (!$part->isVariable()) {
continue;
}
$name = $part->getName();
$value = $part->getVariableValue();
$query_string[] = rawurlencode($name).'='.rawurlencode($value);
}
$query_string = implode('&', $query_string);
$post = $parser->parseQueryString($query_string);
$files = array();
foreach ($parts as $part) {
if ($part->isVariable()) {
continue;
}
$files[$part->getName()] = $part->getPHPFileDictionary();
}
$_FILES = $files;
} else {
$post = $parser->parseQueryString($raw_input);
}
$_POST = $post;
PhabricatorStartup::rebuildRequest();
} else if ($_POST) {
$post = filter_input_array(INPUT_POST, FILTER_UNSAFE_RAW);
if (is_array($post)) {
$_POST = $post;
PhabricatorStartup::rebuildRequest();
}
}
}
}
diff --git a/src/aphront/response/AphrontAjaxResponse.php b/src/aphront/response/AphrontAjaxResponse.php
index 1ccb3fe97..2187defc8 100644
--- a/src/aphront/response/AphrontAjaxResponse.php
+++ b/src/aphront/response/AphrontAjaxResponse.php
@@ -1,91 +1,89 @@
<?php
final class AphrontAjaxResponse extends AphrontResponse {
private $content;
private $error;
private $disableConsole;
public function setContent($content) {
$this->content = $content;
return $this;
}
public function setError($error) {
$this->error = $error;
return $this;
}
public function setDisableConsole($disable) {
$this->disableConsole = $disable;
return $this;
}
private function getConsole() {
if ($this->disableConsole) {
$console = null;
} else {
$request = $this->getRequest();
$console = $request->getApplicationConfiguration()->getConsole();
}
return $console;
}
public function buildResponseString() {
+ $request = $this->getRequest();
$console = $this->getConsole();
if ($console) {
// NOTE: We're stripping query parameters here both for readability and
// to mitigate BREACH and similar attacks. The parameters are available
// in the "Request" tab, so this should not impact usability. See T3684.
- $uri = $this->getRequest()->getRequestURI();
- $uri = new PhutilURI($uri);
- $uri->setQueryParams(array());
+ $path = $request->getPath();
Javelin::initBehavior(
'dark-console',
array(
- 'uri' => (string)$uri,
- 'key' => $console->getKey($this->getRequest()),
+ 'uri' => $path,
+ 'key' => $console->getKey($request),
'color' => $console->getColor(),
- 'quicksand' => $this->getRequest()->isQuicksand(),
+ 'quicksand' => $request->isQuicksand(),
));
}
// Flatten the response first, so we initialize any behaviors and metadata
// we need to.
$content = array(
'payload' => $this->content,
);
$this->encodeJSONForHTTPResponse($content);
$response = CelerityAPI::getStaticResourceResponse();
- $request = $this->getRequest();
if ($request) {
$viewer = $request->getViewer();
if ($viewer) {
$postprocessor_key = $viewer->getUserSetting(
PhabricatorAccessibilitySetting::SETTINGKEY);
if (strlen($postprocessor_key)) {
$response->setPostprocessorKey($postprocessor_key);
}
}
}
$object = $response->buildAjaxResponse(
$content['payload'],
$this->error);
$response_json = $this->encodeJSONForHTTPResponse($object);
return $this->addJSONShield($response_json);
}
public function getHeaders() {
$headers = array(
array('Content-Type', 'text/plain; charset=UTF-8'),
);
$headers = array_merge(parent::getHeaders(), $headers);
return $headers;
}
}
diff --git a/src/aphront/response/AphrontResponse.php b/src/aphront/response/AphrontResponse.php
index 0eab91b2a..9cfda0f5c 100644
--- a/src/aphront/response/AphrontResponse.php
+++ b/src/aphront/response/AphrontResponse.php
@@ -1,434 +1,434 @@
<?php
abstract class AphrontResponse extends Phobject {
private $request;
private $cacheable = false;
private $canCDN;
private $responseCode = 200;
private $lastModified = null;
private $contentSecurityPolicyURIs;
private $disableContentSecurityPolicy;
protected $frameable;
public function setRequest($request) {
$this->request = $request;
return $this;
}
public function getRequest() {
return $this->request;
}
final public function addContentSecurityPolicyURI($kind, $uri) {
if ($this->contentSecurityPolicyURIs === null) {
$this->contentSecurityPolicyURIs = array(
'script-src' => array(),
'connect-src' => array(),
'frame-src' => array(),
'form-action' => array(),
'object-src' => array(),
);
}
if (!isset($this->contentSecurityPolicyURIs[$kind])) {
throw new Exception(
pht(
'Unknown Content-Security-Policy URI kind "%s".',
$kind));
}
$this->contentSecurityPolicyURIs[$kind][] = (string)$uri;
return $this;
}
final public function setDisableContentSecurityPolicy($disable) {
$this->disableContentSecurityPolicy = $disable;
return $this;
}
/* -( Content )------------------------------------------------------------ */
public function getContentIterator() {
// By default, make sure responses are truly returning a string, not some
// kind of object that behaves like a string.
// We're going to remove the execution time limit before dumping the
// response into the sink, and want any rendering that's going to occur
// to happen BEFORE we release the limit.
return array(
(string)$this->buildResponseString(),
);
}
public function buildResponseString() {
throw new PhutilMethodNotImplementedException();
}
/* -( Metadata )----------------------------------------------------------- */
public function getHeaders() {
$headers = array();
if (!$this->frameable) {
$headers[] = array('X-Frame-Options', 'Deny');
}
if ($this->getRequest() && $this->getRequest()->isHTTPS()) {
$hsts_key = 'security.strict-transport-security';
$use_hsts = PhabricatorEnv::getEnvConfig($hsts_key);
if ($use_hsts) {
$duration = phutil_units('365 days in seconds');
} else {
// If HSTS has been disabled, tell browsers to turn it off. This may
// not be effective because we can only disable it over a valid HTTPS
// connection, but it best represents the configured intent.
$duration = 0;
}
$headers[] = array(
'Strict-Transport-Security',
"max-age={$duration}; includeSubdomains; preload",
);
}
$csp = $this->newContentSecurityPolicyHeader();
if ($csp !== null) {
$headers[] = array('Content-Security-Policy', $csp);
}
$headers[] = array('Referrer-Policy', 'no-referrer');
return $headers;
}
private function newContentSecurityPolicyHeader() {
if ($this->disableContentSecurityPolicy) {
return null;
}
// NOTE: We may return a response during preflight checks (for example,
// if a user has a bad version of PHP).
// In this case, setup isn't complete yet and we can't access environmental
// configuration. If we aren't able to read the environment, just decline
// to emit a Content-Security-Policy header.
try {
$cdn = PhabricatorEnv::getEnvConfig('security.alternate-file-domain');
$base_uri = PhabricatorEnv::getURI('/');
} catch (Exception $ex) {
return null;
}
$csp = array();
if ($cdn) {
$default = $this->newContentSecurityPolicySource($cdn);
} else {
// If an alternate file domain is not configured and the user is viewing
// a Phame blog on a custom domain or some other custom site, we'll still
// serve resources from the main site. Include the main site explicitly.
$base_uri = $this->newContentSecurityPolicySource($base_uri);
$default = "'self' {$base_uri}";
}
$csp[] = "default-src {$default}";
// We use "data:" URIs to inline small images into CSS. This policy allows
// "data:" URIs to be used anywhere, but there doesn't appear to be a way
// to say that "data:" URIs are okay in CSS files but not in the document.
$csp[] = "img-src {$default} data:";
// We use inline style="..." attributes in various places, many of which
// are legitimate. We also currently use a <style> tag to implement the
// "Monospaced Font Preference" setting.
$csp[] = "style-src {$default} 'unsafe-inline'";
// On a small number of pages, including the Stripe workflow and the
// ReCAPTCHA challenge, we embed external Javascript directly.
$csp[] = $this->newContentSecurityPolicy('script-src', $default);
// We need to specify that we can connect to ourself in order for AJAX
// requests to work.
$csp[] = $this->newContentSecurityPolicy('connect-src', "'self'");
// DarkConsole and PHPAST both use frames to render some content.
$csp[] = $this->newContentSecurityPolicy('frame-src', "'self'");
// This is a more modern flavor of of "X-Frame-Options" and prevents
// clickjacking attacks where the page is included in a tiny iframe and
// the user is convinced to click a element on the page, which really
// clicks a dangerous button hidden under a picture of a cat.
if ($this->frameable) {
$csp[] = "frame-ancestors 'self'";
} else {
$csp[] = "frame-ancestors 'none'";
}
// Block relics of the old world: Flash, Java applets, and so on. Note
// that Chrome prevents the user from viewing PDF documents if they are
// served with a policy which excludes the domain they are served from.
$csp[] = $this->newContentSecurityPolicy('object-src', "'none'");
// Don't allow forms to submit offsite.
// This can result in some trickiness with file downloads if applications
// try to start downloads by submitting a dialog. Redirect to the file's
// download URI instead of submitting a form to it.
$csp[] = $this->newContentSecurityPolicy('form-action', "'self'");
// Block use of "<base>" to change the origin of relative URIs on the page.
$csp[] = "base-uri 'none'";
$csp = implode('; ', $csp);
return $csp;
}
private function newContentSecurityPolicy($type, $defaults) {
if ($defaults === null) {
$sources = array();
} else {
$sources = (array)$defaults;
}
$uris = $this->contentSecurityPolicyURIs;
if (isset($uris[$type])) {
foreach ($uris[$type] as $uri) {
$sources[] = $this->newContentSecurityPolicySource($uri);
}
}
$sources = array_unique($sources);
return $type.' '.implode(' ', $sources);
}
private function newContentSecurityPolicySource($uri) {
// Some CSP URIs are ultimately user controlled (like notification server
// URIs and CDN URIs) so attempt to stop an attacker from injecting an
// unsafe source (like 'unsafe-eval') into the CSP header.
$uri = id(new PhutilURI($uri))
->setPath(null)
->setFragment(null)
- ->setQueryParams(array());
+ ->removeAllQueryParams();
$uri = (string)$uri;
if (preg_match('/[ ;\']/', $uri)) {
throw new Exception(
pht(
'Attempting to emit a response with an unsafe source ("%s") in the '.
'Content-Security-Policy header.',
$uri));
}
return $uri;
}
public function setCacheDurationInSeconds($duration) {
$this->cacheable = $duration;
return $this;
}
public function setCanCDN($can_cdn) {
$this->canCDN = $can_cdn;
return $this;
}
public function setLastModified($epoch_timestamp) {
$this->lastModified = $epoch_timestamp;
return $this;
}
public function setHTTPResponseCode($code) {
$this->responseCode = $code;
return $this;
}
public function getHTTPResponseCode() {
return $this->responseCode;
}
public function getHTTPResponseMessage() {
switch ($this->getHTTPResponseCode()) {
case 100: return 'Continue';
case 101: return 'Switching Protocols';
case 200: return 'OK';
case 201: return 'Created';
case 202: return 'Accepted';
case 203: return 'Non-Authoritative Information';
case 204: return 'No Content';
case 205: return 'Reset Content';
case 206: return 'Partial Content';
case 300: return 'Multiple Choices';
case 301: return 'Moved Permanently';
case 302: return 'Found';
case 303: return 'See Other';
case 304: return 'Not Modified';
case 305: return 'Use Proxy';
case 306: return 'Switch Proxy';
case 307: return 'Temporary Redirect';
case 400: return 'Bad Request';
case 401: return 'Unauthorized';
case 402: return 'Payment Required';
case 403: return 'Forbidden';
case 404: return 'Not Found';
case 405: return 'Method Not Allowed';
case 406: return 'Not Acceptable';
case 407: return 'Proxy Authentication Required';
case 408: return 'Request Timeout';
case 409: return 'Conflict';
case 410: return 'Gone';
case 411: return 'Length Required';
case 412: return 'Precondition Failed';
case 413: return 'Request Entity Too Large';
case 414: return 'Request-URI Too Long';
case 415: return 'Unsupported Media Type';
case 416: return 'Requested Range Not Satisfiable';
case 417: return 'Expectation Failed';
case 418: return "I'm a teapot";
case 426: return 'Upgrade Required';
case 500: return 'Internal Server Error';
case 501: return 'Not Implemented';
case 502: return 'Bad Gateway';
case 503: return 'Service Unavailable';
case 504: return 'Gateway Timeout';
case 505: return 'HTTP Version Not Supported';
default: return '';
}
}
public function setFrameable($frameable) {
$this->frameable = $frameable;
return $this;
}
public static function processValueForJSONEncoding(&$value, $key) {
if ($value instanceof PhutilSafeHTMLProducerInterface) {
// This renders the producer down to PhutilSafeHTML, which will then
// be simplified into a string below.
$value = hsprintf('%s', $value);
}
if ($value instanceof PhutilSafeHTML) {
// TODO: Javelin supports implicity conversion of '__html' objects to
// JX.HTML, but only for Ajax responses, not behaviors. Just leave things
// as they are for now (where behaviors treat responses as HTML or plain
// text at their discretion).
$value = $value->getHTMLContent();
}
}
public static function encodeJSONForHTTPResponse(array $object) {
array_walk_recursive(
$object,
array(__CLASS__, 'processValueForJSONEncoding'));
$response = phutil_json_encode($object);
// Prevent content sniffing attacks by encoding "<" and ">", so browsers
// won't try to execute the document as HTML even if they ignore
// Content-Type and X-Content-Type-Options. See T865.
$response = str_replace(
array('<', '>'),
array('\u003c', '\u003e'),
$response);
return $response;
}
protected function addJSONShield($json_response) {
// Add a shield to prevent "JSON Hijacking" attacks where an attacker
// requests a JSON response using a normal <script /> tag and then uses
// Object.prototype.__defineSetter__() or similar to read response data.
// This header causes the browser to loop infinitely instead of handing over
// sensitive data.
$shield = 'for (;;);';
$response = $shield.$json_response;
return $response;
}
public function getCacheHeaders() {
$headers = array();
if ($this->cacheable) {
$cache_control = array();
$cache_control[] = sprintf('max-age=%d', $this->cacheable);
if ($this->canCDN) {
$cache_control[] = 'public';
} else {
$cache_control[] = 'private';
}
$headers[] = array(
'Cache-Control',
implode(', ', $cache_control),
);
$headers[] = array(
'Expires',
$this->formatEpochTimestampForHTTPHeader(time() + $this->cacheable),
);
} else {
$headers[] = array(
'Cache-Control',
'no-store',
);
$headers[] = array(
'Expires',
'Sat, 01 Jan 2000 00:00:00 GMT',
);
}
if ($this->lastModified) {
$headers[] = array(
'Last-Modified',
$this->formatEpochTimestampForHTTPHeader($this->lastModified),
);
}
// IE has a feature where it may override an explicit Content-Type
// declaration by inferring a content type. This can be a security risk
// and we always explicitly transmit the correct Content-Type header, so
// prevent IE from using inferred content types. This only offers protection
// on recent versions of IE; IE6/7 and Opera currently ignore this header.
$headers[] = array('X-Content-Type-Options', 'nosniff');
return $headers;
}
private function formatEpochTimestampForHTTPHeader($epoch_timestamp) {
return gmdate('D, d M Y H:i:s', $epoch_timestamp).' GMT';
}
protected function shouldCompressResponse() {
return true;
}
public function willBeginWrite() {
if ($this->shouldCompressResponse()) {
// Enable automatic compression here. Webservers sometimes do this for
// us, but we now detect the absence of compression and warn users about
// it so try to cover our bases more thoroughly.
ini_set('zlib.output_compression', 1);
} else {
ini_set('zlib.output_compression', 0);
}
}
public function didCompleteWrite($aborted) {
return;
}
}
diff --git a/src/aphront/response/AphrontUnhandledExceptionResponse.php b/src/aphront/response/AphrontUnhandledExceptionResponse.php
index efd9d70ea..32d612ca5 100644
--- a/src/aphront/response/AphrontUnhandledExceptionResponse.php
+++ b/src/aphront/response/AphrontUnhandledExceptionResponse.php
@@ -1,94 +1,133 @@
<?php
final class AphrontUnhandledExceptionResponse
extends AphrontStandaloneHTMLResponse {
private $exception;
+ private $showStackTraces;
+
+ public function setShowStackTraces($show_stack_traces) {
+ $this->showStackTraces = $show_stack_traces;
+ return $this;
+ }
+
+ public function getShowStackTraces() {
+ return $this->showStackTraces;
+ }
+
+ public function setException($exception) {
+ // NOTE: We accept an Exception or a Throwable.
- public function setException(Exception $exception) {
// Log the exception unless it's specifically a silent malformed request
// exception.
$should_log = true;
if ($exception instanceof AphrontMalformedRequestException) {
if ($exception->getIsUnlogged()) {
$should_log = false;
}
}
if ($should_log) {
phlog($exception);
}
$this->exception = $exception;
return $this;
}
public function getHTTPResponseCode() {
return 500;
}
protected function getResources() {
return array(
'css/application/config/config-template.css',
'css/application/config/unhandled-exception.css',
);
}
protected function getResponseTitle() {
$ex = $this->exception;
if ($ex instanceof AphrontMalformedRequestException) {
return $ex->getTitle();
} else {
return pht('Unhandled Exception');
}
}
protected function getResponseBodyClass() {
return 'unhandled-exception';
}
protected function getResponseBody() {
$ex = $this->exception;
if ($ex instanceof AphrontMalformedRequestException) {
$title = $ex->getTitle();
} else {
$title = get_class($ex);
}
$body = $ex->getMessage();
$body = phutil_escape_html_newlines($body);
+ $classes = array();
+ $classes[] = 'unhandled-exception-detail';
+
+ $stack = null;
+ if ($this->getShowStackTraces()) {
+ try {
+ $stack = id(new AphrontStackTraceView())
+ ->setTrace($ex->getTrace());
+
+ $stack = hsprintf('%s', $stack);
+
+ $stack = phutil_tag(
+ 'div',
+ array(
+ 'class' => 'unhandled-exception-stack',
+ ),
+ $stack);
+
+ $classes[] = 'unhandled-exception-with-stack';
+ } catch (Exception $trace_exception) {
+ $stack = null;
+ } catch (Throwable $trace_exception) {
+ $stack = null;
+ }
+ }
+
return phutil_tag(
'div',
array(
- 'class' => 'unhandled-exception-detail',
+ 'class' => implode(' ', $classes),
),
array(
phutil_tag(
'h1',
array(
'class' => 'unhandled-exception-title',
),
$title),
phutil_tag(
'div',
array(
'class' => 'unhandled-exception-body',
),
$body),
+ $stack,
));
}
protected function buildPlainTextResponseString() {
$ex = $this->exception;
return pht(
'%s: %s',
get_class($ex),
$ex->getMessage());
}
}
diff --git a/src/aphront/sink/AphrontHTTPSink.php b/src/aphront/sink/AphrontHTTPSink.php
index 51c54df52..9e43e4a68 100644
--- a/src/aphront/sink/AphrontHTTPSink.php
+++ b/src/aphront/sink/AphrontHTTPSink.php
@@ -1,153 +1,172 @@
<?php
/**
* Abstract class which wraps some sort of output mechanism for HTTP responses.
* Normally this is just @{class:AphrontPHPHTTPSink}, which uses "echo" and
* "header()" to emit responses.
*
- * Mostly, this class allows us to do install security or metrics hooks in the
- * output pipeline.
- *
* @task write Writing Response Components
* @task emit Emitting the Response
*/
abstract class AphrontHTTPSink extends Phobject {
+ private $showStackTraces = false;
+
+ final public function setShowStackTraces($show_stack_traces) {
+ $this->showStackTraces = $show_stack_traces;
+ return $this;
+ }
+
+ final public function getShowStackTraces() {
+ return $this->showStackTraces;
+ }
+
/* -( Writing Response Components )---------------------------------------- */
/**
* Write an HTTP status code to the output.
*
* @param int Numeric HTTP status code.
* @return void
*/
final public function writeHTTPStatus($code, $message = '') {
if (!preg_match('/^\d{3}$/', $code)) {
throw new Exception(pht("Malformed HTTP status code '%s'!", $code));
}
$code = (int)$code;
$this->emitHTTPStatus($code, $message);
}
/**
* Write HTTP headers to the output.
*
* @param list<pair> List of <name, value> pairs.
* @return void
*/
final public function writeHeaders(array $headers) {
foreach ($headers as $header) {
if (!is_array($header) || count($header) !== 2) {
throw new Exception(pht('Malformed header.'));
}
list($name, $value) = $header;
if (strpos($name, ':') !== false) {
throw new Exception(
pht(
'Declining to emit response with malformed HTTP header name: %s',
$name));
}
// Attackers may perform an "HTTP response splitting" attack by making
// the application emit certain types of headers containing newlines:
//
// http://en.wikipedia.org/wiki/HTTP_response_splitting
//
// PHP has built-in protections against HTTP response-splitting, but they
// are of dubious trustworthiness:
//
// http://news.php.net/php.internals/57655
if (preg_match('/[\r\n\0]/', $name.$value)) {
throw new Exception(
pht(
'Declining to emit response with unsafe HTTP header: %s',
"<'".$name."', '".$value."'>."));
}
}
foreach ($headers as $header) {
list($name, $value) = $header;
$this->emitHeader($name, $value);
}
}
/**
* Write HTTP body data to the output.
*
* @param string Body data.
* @return void
*/
final public function writeData($data) {
$this->emitData($data);
}
/**
* Write an entire @{class:AphrontResponse} to the output.
*
* @param AphrontResponse The response object to write.
* @return void
*/
final public function writeResponse(AphrontResponse $response) {
$response->willBeginWrite();
// Build the content iterator first, in case it throws. Ideally, we'd
// prefer to handle exceptions before we emit the response status or any
// HTTP headers.
$data = $response->getContentIterator();
+ // This isn't an exceptionally clean separation of concerns, but we need
+ // to add CSP headers for all response types (including both web pages
+ // and dialogs) and can't determine the correct CSP until after we render
+ // the page (because page elements like Recaptcha may add CSP rules).
+ $static = CelerityAPI::getStaticResourceResponse();
+ foreach ($static->getContentSecurityPolicyURIMap() as $kind => $uris) {
+ foreach ($uris as $uri) {
+ $response->addContentSecurityPolicyURI($kind, $uri);
+ }
+ }
+
$all_headers = array_merge(
$response->getHeaders(),
$response->getCacheHeaders());
$this->writeHTTPStatus(
$response->getHTTPResponseCode(),
$response->getHTTPResponseMessage());
$this->writeHeaders($all_headers);
// Allow clients an unlimited amount of time to download the response.
// This allows clients to perform a "slow loris" attack, where they
// download a large response very slowly to tie up process slots. However,
// concurrent connection limits and "RequestReadTimeout" already prevent
// this attack. We could add our own minimum download rate here if we want
// to make this easier to configure eventually.
// For normal page responses, we've fully rendered the page into a string
// already so all that's left is writing it to the client.
// For unusual responses (like large file downloads) we may still be doing
// some meaningful work, but in theory that work is intrinsic to streaming
// the response.
set_time_limit(0);
$abort = false;
foreach ($data as $block) {
if (!$this->isWritable()) {
$abort = true;
break;
}
$this->writeData($block);
}
$response->didCompleteWrite($abort);
}
/* -( Emitting the Response )---------------------------------------------- */
abstract protected function emitHTTPStatus($code, $message = '');
abstract protected function emitHeader($name, $value);
abstract protected function emitData($data);
abstract protected function isWritable();
}
diff --git a/src/applications/almanac/controller/AlmanacController.php b/src/applications/almanac/controller/AlmanacController.php
index 24a898676..918b3a4ad 100644
--- a/src/applications/almanac/controller/AlmanacController.php
+++ b/src/applications/almanac/controller/AlmanacController.php
@@ -1,215 +1,209 @@
<?php
abstract class AlmanacController
extends PhabricatorController {
protected function buildAlmanacPropertiesTable(
AlmanacPropertyInterface $object) {
$viewer = $this->getViewer();
$properties = $object->getAlmanacProperties();
$this->requireResource('almanac-css');
Javelin::initBehavior('phabricator-tooltips', array());
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
$properties = $object->getAlmanacProperties();
$icon_builtin = id(new PHUIIconView())
->setIcon('fa-circle')
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => pht('Builtin Property'),
'align' => 'E',
));
$icon_custom = id(new PHUIIconView())
->setIcon('fa-circle-o grey')
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => pht('Custom Property'),
'align' => 'E',
));
$builtins = $object->getAlmanacPropertyFieldSpecifications();
$defaults = mpull($builtins, 'getValueForTransaction');
// Sort fields so builtin fields appear first, then fields are ordered
// alphabetically.
$properties = msort($properties, 'getFieldName');
$head = array();
$tail = array();
foreach ($properties as $property) {
$key = $property->getFieldName();
if (isset($builtins[$key])) {
$head[$key] = $property;
} else {
$tail[$key] = $property;
}
}
$properties = $head + $tail;
$delete_base = $this->getApplicationURI('property/delete/');
$edit_base = $this->getApplicationURI('property/update/');
$rows = array();
foreach ($properties as $key => $property) {
$value = $property->getFieldValue();
$is_builtin = isset($builtins[$key]);
$is_persistent = (bool)$property->getID();
- $delete_uri = id(new PhutilURI($delete_base))
- ->setQueryParams(
- array(
- 'key' => $key,
- 'objectPHID' => $object->getPHID(),
- ));
+ $params = array(
+ 'key' => $key,
+ 'objectPHID' => $object->getPHID(),
+ );
- $edit_uri = id(new PhutilURI($edit_base))
- ->setQueryParams(
- array(
- 'key' => $key,
- 'objectPHID' => $object->getPHID(),
- ));
+ $delete_uri = new PhutilURI($delete_base, $params);
+ $edit_uri = new PhutilURI($edit_base, $params);
$delete = javelin_tag(
'a',
array(
'class' => (($can_edit && $is_persistent)
? 'button button-grey small'
: 'button button-grey small disabled'),
'sigil' => 'workflow',
'href' => $delete_uri,
),
$is_builtin ? pht('Reset') : pht('Delete'));
$default = idx($defaults, $key);
$is_default = ($default !== null && $default === $value);
$display_value = PhabricatorConfigJSON::prettyPrintJSON($value);
if ($is_default) {
$display_value = phutil_tag(
'span',
array(
'class' => 'almanac-default-property-value',
),
$display_value);
}
$display_key = $key;
if ($can_edit) {
$display_key = javelin_tag(
'a',
array(
'href' => $edit_uri,
'sigil' => 'workflow',
),
$display_key);
}
$rows[] = array(
($is_builtin ? $icon_builtin : $icon_custom),
$display_key,
$display_value,
$delete,
);
}
$table = id(new AphrontTableView($rows))
->setNoDataString(pht('No properties.'))
->setHeaders(
array(
null,
pht('Name'),
pht('Value'),
null,
))
->setColumnClasses(
array(
null,
null,
'wide',
'action',
));
$phid = $object->getPHID();
$add_uri = id(new PhutilURI($edit_base))
- ->setQueryParam('objectPHID', $object->getPHID());
+ ->replaceQueryParam('objectPHID', $object->getPHID());
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
$add_button = id(new PHUIButtonView())
->setTag('a')
->setHref($add_uri)
->setWorkflow(true)
->setDisabled(!$can_edit)
->setText(pht('Add Property'))
->setIcon('fa-plus');
$header = id(new PHUIHeaderView())
->setHeader(pht('Properties'))
->addActionLink($add_button);
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($table);
}
protected function addClusterMessage(
$positive,
$negative) {
$can_manage = $this->hasApplicationCapability(
AlmanacManageClusterServicesCapability::CAPABILITY);
$doc_link = phutil_tag(
'a',
array(
'href' => PhabricatorEnv::getDoclink(
'Clustering Introduction'),
'target' => '_blank',
),
pht('Learn More'));
if ($can_manage) {
$severity = PHUIInfoView::SEVERITY_NOTICE;
$message = $positive;
} else {
$severity = PHUIInfoView::SEVERITY_WARNING;
$message = $negative;
}
$icon = id(new PHUIIconView())
->setIcon('fa-sitemap');
return id(new PHUIInfoView())
->setSeverity($severity)
->setErrors(
array(
array($icon, ' ', $message, ' ', $doc_link),
));
}
protected function getPropertyDeleteURI($object) {
return null;
}
protected function getPropertyUpdateURI($object) {
return null;
}
}
diff --git a/src/applications/almanac/query/AlmanacDeviceQuery.php b/src/applications/almanac/query/AlmanacDeviceQuery.php
index 0d38070e0..21f8c952e 100644
--- a/src/applications/almanac/query/AlmanacDeviceQuery.php
+++ b/src/applications/almanac/query/AlmanacDeviceQuery.php
@@ -1,146 +1,145 @@
<?php
final class AlmanacDeviceQuery
extends AlmanacQuery {
private $ids;
private $phids;
private $names;
private $namePrefix;
private $nameSuffix;
private $isClusterDevice;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withNamePrefix($prefix) {
$this->namePrefix = $prefix;
return $this;
}
public function withNameSuffix($suffix) {
$this->nameSuffix = $suffix;
return $this;
}
public function withNameNgrams($ngrams) {
return $this->withNgramsConstraint(
new AlmanacDeviceNameNgrams(),
$ngrams);
}
public function withIsClusterDevice($is_cluster_device) {
$this->isClusterDevice = $is_cluster_device;
return $this;
}
public function newResultObject() {
return new AlmanacDevice();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'device.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'device.phid IN (%Ls)',
$this->phids);
}
if ($this->names !== null) {
$hashes = array();
foreach ($this->names as $name) {
$hashes[] = PhabricatorHash::digestForIndex($name);
}
$where[] = qsprintf(
$conn,
'device.nameIndex IN (%Ls)',
$hashes);
}
if ($this->namePrefix !== null) {
$where[] = qsprintf(
$conn,
'device.name LIKE %>',
$this->namePrefix);
}
if ($this->nameSuffix !== null) {
$where[] = qsprintf(
$conn,
'device.name LIKE %<',
$this->nameSuffix);
}
if ($this->isClusterDevice !== null) {
$where[] = qsprintf(
$conn,
'device.isBoundToClusterService = %d',
(int)$this->isClusterDevice);
}
return $where;
}
protected function getPrimaryTableAlias() {
return 'device';
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'name',
'type' => 'string',
'unique' => true,
'reverse' => true,
),
);
}
- protected function getPagingValueMap($cursor, array $keys) {
- $device = $this->loadCursorObject($cursor);
+ protected function newPagingMapFromPartialObject($object) {
return array(
- 'id' => $device->getID(),
- 'name' => $device->getName(),
+ 'id' => (int)$object->getID(),
+ 'name' => $object->getName(),
);
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Device Name'),
),
) + parent::getBuiltinOrders();
}
public function getQueryApplicationClass() {
return 'PhabricatorAlmanacApplication';
}
}
diff --git a/src/applications/almanac/query/AlmanacInterfaceQuery.php b/src/applications/almanac/query/AlmanacInterfaceQuery.php
index d5886761c..5738108ff 100644
--- a/src/applications/almanac/query/AlmanacInterfaceQuery.php
+++ b/src/applications/almanac/query/AlmanacInterfaceQuery.php
@@ -1,200 +1,211 @@
<?php
final class AlmanacInterfaceQuery
extends AlmanacQuery {
private $ids;
private $phids;
private $networkPHIDs;
private $devicePHIDs;
private $addresses;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withNetworkPHIDs(array $phids) {
$this->networkPHIDs = $phids;
return $this;
}
public function withDevicePHIDs(array $phids) {
$this->devicePHIDs = $phids;
return $this;
}
public function withAddresses(array $addresses) {
$this->addresses = $addresses;
return $this;
}
public function newResultObject() {
return new AlmanacInterface();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $interfaces) {
$network_phids = mpull($interfaces, 'getNetworkPHID');
$device_phids = mpull($interfaces, 'getDevicePHID');
$networks = id(new AlmanacNetworkQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($network_phids)
->needProperties($this->getNeedProperties())
->execute();
$networks = mpull($networks, null, 'getPHID');
$devices = id(new AlmanacDeviceQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($device_phids)
->needProperties($this->getNeedProperties())
->execute();
$devices = mpull($devices, null, 'getPHID');
foreach ($interfaces as $key => $interface) {
$network = idx($networks, $interface->getNetworkPHID());
$device = idx($devices, $interface->getDevicePHID());
if (!$network || !$device) {
$this->didRejectResult($interface);
unset($interfaces[$key]);
continue;
}
$interface->attachNetwork($network);
$interface->attachDevice($device);
}
return $interfaces;
}
+ protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
+ $select = parent::buildSelectClauseParts($conn);
+
+ if ($this->shouldJoinDeviceTable()) {
+ $select[] = qsprintf($conn, 'device.name');
+ }
+
+ return $select;
+ }
+
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'interface.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'interface.phid IN (%Ls)',
$this->phids);
}
if ($this->networkPHIDs !== null) {
$where[] = qsprintf(
$conn,
'interface.networkPHID IN (%Ls)',
$this->networkPHIDs);
}
if ($this->devicePHIDs !== null) {
$where[] = qsprintf(
$conn,
'interface.devicePHID IN (%Ls)',
$this->devicePHIDs);
}
if ($this->addresses !== null) {
$parts = array();
foreach ($this->addresses as $address) {
$parts[] = qsprintf(
$conn,
'(interface.networkPHID = %s '.
'AND interface.address = %s '.
'AND interface.port = %d)',
$address->getNetworkPHID(),
$address->getAddress(),
$address->getPort());
}
$where[] = qsprintf($conn, '%LO', $parts);
}
return $where;
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->shouldJoinDeviceTable()) {
$joins[] = qsprintf(
$conn,
'JOIN %T device ON device.phid = interface.devicePHID',
id(new AlmanacDevice())->getTableName());
}
return $joins;
}
protected function shouldGroupQueryResultRows() {
if ($this->shouldJoinDeviceTable()) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
private function shouldJoinDeviceTable() {
$vector = $this->getOrderVector();
if ($vector->containsKey('name')) {
return true;
}
return false;
}
protected function getPrimaryTableAlias() {
return 'interface';
}
public function getQueryApplicationClass() {
return 'PhabricatorAlmanacApplication';
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name', 'id'),
'name' => pht('Device Name'),
),
) + parent::getBuiltinOrders();
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => 'device',
'column' => 'name',
'type' => 'string',
'reverse' => true,
),
);
}
- protected function getPagingValueMap($cursor, array $keys) {
- $interface = $this->loadCursorObject($cursor);
+ protected function newPagingMapFromCursorObject(
+ PhabricatorQueryCursor $cursor,
+ array $keys) {
- $map = array(
- 'id' => $interface->getID(),
- 'name' => $interface->getDevice()->getName(),
- );
+ $interface = $cursor->getObject();
- return $map;
+ return array(
+ 'id' => (int)$interface->getID(),
+ 'name' => $cursor->getRawRowProperty('device.name'),
+ );
}
}
diff --git a/src/applications/almanac/query/AlmanacNamespaceQuery.php b/src/applications/almanac/query/AlmanacNamespaceQuery.php
index 81332cf03..d4378e17c 100644
--- a/src/applications/almanac/query/AlmanacNamespaceQuery.php
+++ b/src/applications/almanac/query/AlmanacNamespaceQuery.php
@@ -1,103 +1,102 @@
<?php
final class AlmanacNamespaceQuery
extends AlmanacQuery {
private $ids;
private $phids;
private $names;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withNameNgrams($ngrams) {
return $this->withNgramsConstraint(
new AlmanacNamespaceNameNgrams(),
$ngrams);
}
public function newResultObject() {
return new AlmanacNamespace();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'namespace.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'namespace.phid IN (%Ls)',
$this->phids);
}
if ($this->names !== null) {
$where[] = qsprintf(
$conn,
'namespace.name IN (%Ls)',
$this->names);
}
return $where;
}
protected function getPrimaryTableAlias() {
return 'namespace';
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'name',
'type' => 'string',
'unique' => true,
'reverse' => true,
),
);
}
- protected function getPagingValueMap($cursor, array $keys) {
- $namespace = $this->loadCursorObject($cursor);
+ protected function newPagingMapFromPartialObject($object) {
return array(
- 'id' => $namespace->getID(),
- 'name' => $namespace->getName(),
+ 'id' => (int)$object->getID(),
+ 'name' => $object->getName(),
);
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Namespace Name'),
),
) + parent::getBuiltinOrders();
}
public function getQueryApplicationClass() {
return 'PhabricatorAlmanacApplication';
}
}
diff --git a/src/applications/almanac/query/AlmanacServiceQuery.php b/src/applications/almanac/query/AlmanacServiceQuery.php
index 3374413e5..edc55276a 100644
--- a/src/applications/almanac/query/AlmanacServiceQuery.php
+++ b/src/applications/almanac/query/AlmanacServiceQuery.php
@@ -1,226 +1,225 @@
<?php
final class AlmanacServiceQuery
extends AlmanacQuery {
private $ids;
private $phids;
private $names;
private $serviceTypes;
private $devicePHIDs;
private $namePrefix;
private $nameSuffix;
private $needBindings;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withServiceTypes(array $types) {
$this->serviceTypes = $types;
return $this;
}
public function withDevicePHIDs(array $phids) {
$this->devicePHIDs = $phids;
return $this;
}
public function withNamePrefix($prefix) {
$this->namePrefix = $prefix;
return $this;
}
public function withNameSuffix($suffix) {
$this->nameSuffix = $suffix;
return $this;
}
public function withNameNgrams($ngrams) {
return $this->withNgramsConstraint(
new AlmanacServiceNameNgrams(),
$ngrams);
}
public function needBindings($need_bindings) {
$this->needBindings = $need_bindings;
return $this;
}
public function newResultObject() {
return new AlmanacService();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->shouldJoinBindingTable()) {
$joins[] = qsprintf(
$conn,
'JOIN %T binding ON service.phid = binding.servicePHID',
id(new AlmanacBinding())->getTableName());
}
return $joins;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'service.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'service.phid IN (%Ls)',
$this->phids);
}
if ($this->names !== null) {
$hashes = array();
foreach ($this->names as $name) {
$hashes[] = PhabricatorHash::digestForIndex($name);
}
$where[] = qsprintf(
$conn,
'service.nameIndex IN (%Ls)',
$hashes);
}
if ($this->serviceTypes !== null) {
$where[] = qsprintf(
$conn,
'service.serviceType IN (%Ls)',
$this->serviceTypes);
}
if ($this->devicePHIDs !== null) {
$where[] = qsprintf(
$conn,
'binding.devicePHID IN (%Ls)',
$this->devicePHIDs);
}
if ($this->namePrefix !== null) {
$where[] = qsprintf(
$conn,
'service.name LIKE %>',
$this->namePrefix);
}
if ($this->nameSuffix !== null) {
$where[] = qsprintf(
$conn,
'service.name LIKE %<',
$this->nameSuffix);
}
return $where;
}
protected function willFilterPage(array $services) {
$service_map = AlmanacServiceType::getAllServiceTypes();
foreach ($services as $key => $service) {
$implementation = idx($service_map, $service->getServiceType());
if (!$implementation) {
$this->didRejectResult($service);
unset($services[$key]);
continue;
}
$implementation = clone $implementation;
$service->attachServiceImplementation($implementation);
}
return $services;
}
protected function didFilterPage(array $services) {
if ($this->needBindings) {
$service_phids = mpull($services, 'getPHID');
$bindings = id(new AlmanacBindingQuery())
->setViewer($this->getViewer())
->withServicePHIDs($service_phids)
->needProperties($this->getNeedProperties())
->execute();
$bindings = mgroup($bindings, 'getServicePHID');
foreach ($services as $service) {
$service_bindings = idx($bindings, $service->getPHID(), array());
$service->attachBindings($service_bindings);
}
}
return parent::didFilterPage($services);
}
private function shouldJoinBindingTable() {
return ($this->devicePHIDs !== null);
}
protected function shouldGroupQueryResultRows() {
if ($this->shouldJoinBindingTable()) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
protected function getPrimaryTableAlias() {
return 'service';
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'name',
'type' => 'string',
'unique' => true,
'reverse' => true,
),
);
}
- protected function getPagingValueMap($cursor, array $keys) {
- $service = $this->loadCursorObject($cursor);
+ protected function newPagingMapFromPartialObject($object) {
return array(
- 'id' => $service->getID(),
- 'name' => $service->getName(),
+ 'id' => (int)$object->getID(),
+ 'name' => $object->getName(),
);
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Service Name'),
),
) + parent::getBuiltinOrders();
}
}
diff --git a/src/applications/almanac/storage/AlmanacModularTransaction.php b/src/applications/almanac/storage/AlmanacModularTransaction.php
index 649706924..3e2eb0a3e 100644
--- a/src/applications/almanac/storage/AlmanacModularTransaction.php
+++ b/src/applications/almanac/storage/AlmanacModularTransaction.php
@@ -1,14 +1,10 @@
<?php
abstract class AlmanacModularTransaction
extends PhabricatorModularTransaction {
public function getApplicationName() {
return 'almanac';
}
- public function getApplicationTransactionCommentObject() {
- return null;
- }
-
}
diff --git a/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php b/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php
index 23583422f..7d14df723 100644
--- a/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php
+++ b/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php
@@ -1,285 +1,321 @@
<?php
final class PhabricatorAuditManagementDeleteWorkflow
extends PhabricatorAuditManagementWorkflow {
protected function didConstruct() {
$this
->setName('delete')
->setExamples('**delete** [--dry-run] ...')
->setSynopsis(pht('Delete audit requests matching parameters.'))
->setArguments(
array(
array(
'name' => 'dry-run',
'help' => pht(
'Show what would be deleted, but do not actually delete '.
'anything.'),
),
array(
'name' => 'users',
'param' => 'names',
'help' => pht('Select only audits by a given list of users.'),
),
array(
'name' => 'repositories',
'param' => 'repos',
'help' => pht(
'Select only audits in a given list of repositories.'),
),
array(
'name' => 'commits',
'param' => 'commits',
'help' => pht('Select only audits for the given commits.'),
),
array(
'name' => 'min-commit-date',
'param' => 'date',
'help' => pht(
'Select only audits for commits on or after the given date.'),
),
array(
'name' => 'max-commit-date',
'param' => 'date',
'help' => pht(
'Select only audits for commits on or before the given date.'),
),
array(
'name' => 'status',
'param' => 'status',
'help' => pht(
'Select only audits in the given status. By default, '.
'only open audits are selected.'),
),
array(
'name' => 'ids',
'param' => 'ids',
'help' => pht('Select only audits with the given IDs.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$viewer = $this->getViewer();
$users = $this->loadUsers($args->getArg('users'));
$repos = $this->loadRepos($args->getArg('repositories'));
$commits = $this->loadCommits($args->getArg('commits'));
$ids = $this->parseList($args->getArg('ids'));
$status = $args->getArg('status');
$min_date = $this->loadDate($args->getArg('min-commit-date'));
$max_date = $this->loadDate($args->getArg('max-commit-date'));
if ($min_date && $max_date && ($min_date > $max_date)) {
throw new PhutilArgumentUsageException(
pht('Specified maximum date must come after specified minimum date.'));
}
$is_dry_run = $args->getArg('dry-run');
$query = id(new DiffusionCommitQuery())
->setViewer($this->getViewer())
->needAuditRequests(true);
if ($status) {
$query->withStatuses(array($status));
}
$id_map = array();
if ($ids) {
$id_map = array_fuse($ids);
$query->withAuditIDs($ids);
}
if ($repos) {
$query->withRepositoryIDs(mpull($repos, 'getID'));
}
$auditor_map = array();
if ($users) {
$auditor_map = array_fuse(mpull($users, 'getPHID'));
$query->withAuditorPHIDs($auditor_map);
}
if ($commits) {
$query->withPHIDs(mpull($commits, 'getPHID'));
}
- $commits = $query->execute();
- $commits = mpull($commits, null, 'getPHID');
+ $commit_iterator = new PhabricatorQueryIterator($query);
+
$audits = array();
- foreach ($commits as $commit) {
+ foreach ($commit_iterator as $commit) {
$commit_audits = $commit->getAudits();
foreach ($commit_audits as $key => $audit) {
if ($id_map && empty($id_map[$audit->getID()])) {
unset($commit_audits[$key]);
continue;
}
if ($auditor_map && empty($auditor_map[$audit->getAuditorPHID()])) {
unset($commit_audits[$key]);
continue;
}
if ($min_date && $commit->getEpoch() < $min_date) {
unset($commit_audits[$key]);
continue;
}
if ($max_date && $commit->getEpoch() > $max_date) {
unset($commit_audits[$key]);
continue;
}
}
- $audits[] = $commit_audits;
- }
- $audits = array_mergev($audits);
- $console = PhutilConsole::getConsole();
-
- if (!$audits) {
- $console->writeErr("%s\n", pht('No audits match the query.'));
- return 0;
- }
-
- $handles = id(new PhabricatorHandleQuery())
- ->setViewer($this->getViewer())
- ->withPHIDs(mpull($audits, 'getAuditorPHID'))
- ->execute();
+ if (!$commit_audits) {
+ continue;
+ }
+ $handles = id(new PhabricatorHandleQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(mpull($commit_audits, 'getAuditorPHID'))
+ ->execute();
- foreach ($audits as $audit) {
- $commit = $commits[$audit->getCommitPHID()];
+ foreach ($commit_audits as $audit) {
+ $audit_id = $audit->getID();
- $console->writeOut(
- "%s\n",
- sprintf(
+ $description = sprintf(
'%10d %-16s %-16s %s: %s',
- $audit->getID(),
+ $audit_id,
$handles[$audit->getAuditorPHID()]->getName(),
PhabricatorAuditStatusConstants::getStatusName(
$audit->getAuditStatus()),
$commit->getRepository()->formatCommitName(
$commit->getCommitIdentifier()),
- trim($commit->getSummary())));
+ trim($commit->getSummary()));
+
+ $audits[] = array(
+ 'auditID' => $audit_id,
+ 'commitPHID' => $commit->getPHID(),
+ 'description' => $description,
+ );
+ }
}
- if (!$is_dry_run) {
- $message = pht(
- 'Really delete these %d audit(s)? They will be permanently deleted '.
- 'and can not be recovered.',
- count($audits));
- if ($console->confirm($message)) {
- foreach ($audits as $audit) {
- $id = $audit->getID();
- $console->writeOut("%s\n", pht('Deleting audit %d...', $id));
- $audit->delete();
- }
+ if (!$audits) {
+ echo tsprintf(
+ "%s\n",
+ pht('No audits match the query.'));
+ return 0;
+ }
+
+ foreach ($audits as $audit_spec) {
+ echo tsprintf(
+ "%s\n",
+ $audit_spec['description']);
+ }
+
+ if ($is_dry_run) {
+ echo tsprintf(
+ "%s\n",
+ pht('This is a dry run, so no changes will be made.'));
+ return 0;
+ }
+
+ $message = pht(
+ 'Really delete these %s audit(s)? They will be permanently deleted '.
+ 'and can not be recovered.',
+ phutil_count($audits));
+ if (!phutil_console_confirm($message)) {
+ echo tsprintf(
+ "%s\n",
+ pht('User aborted the workflow.'));
+ return 1;
+ }
+
+ $audits_by_commit = igroup($audits, 'commitPHID');
+ foreach ($audits_by_commit as $commit_phid => $audit_specs) {
+ $audit_ids = ipull($audit_specs, 'auditID');
+
+ $audits = id(new PhabricatorRepositoryAuditRequest())->loadAllWhere(
+ 'id IN (%Ld)',
+ $audit_ids);
+
+ foreach ($audits as $audit) {
+ $id = $audit->getID();
+
+ echo tsprintf(
+ "%s\n",
+ pht('Deleting audit %d...', $id));
+
+ $audit->delete();
}
+
+ $this->synchronizeCommitAuditState($commit_phid);
}
return 0;
}
private function loadUsers($users) {
$users = $this->parseList($users);
if (!$users) {
return null;
}
$objects = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withUsernames($users)
->execute();
$objects = mpull($objects, null, 'getUsername');
foreach ($users as $name) {
if (empty($objects[$name])) {
throw new PhutilArgumentUsageException(
pht('No such user with username "%s"!', $name));
}
}
return $objects;
}
private function parseList($list) {
$list = preg_split('/\s*,\s*/', $list);
foreach ($list as $key => $item) {
$list[$key] = trim($item);
}
foreach ($list as $key => $item) {
if (!strlen($item)) {
unset($list[$key]);
}
}
return $list;
}
private function loadRepos($identifiers) {
$identifiers = $this->parseList($identifiers);
if (!$identifiers) {
return null;
}
$query = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withIdentifiers($identifiers);
$repos = $query->execute();
$map = $query->getIdentifierMap();
foreach ($identifiers as $identifier) {
if (empty($map[$identifier])) {
throw new PhutilArgumentUsageException(
pht('No repository "%s" exists!', $identifier));
}
}
return $repos;
}
private function loadDate($date) {
if (!$date) {
return null;
}
$epoch = strtotime($date);
if (!$epoch || $epoch < 1) {
throw new PhutilArgumentUsageException(
pht(
'Unable to parse date "%s". Use a format like "%s".',
$date,
'2000-01-01'));
}
return $epoch;
}
private function loadCommits($commits) {
$names = $this->parseList($commits);
if (!$names) {
return null;
}
$query = id(new DiffusionCommitQuery())
->setViewer($this->getViewer())
->withIdentifiers($names);
$commits = $query->execute();
$map = $query->getIdentifierMap();
foreach ($names as $name) {
if (empty($map[$name])) {
throw new PhutilArgumentUsageException(
pht('No such commit "%s"!', $name));
}
}
return $commits;
}
}
diff --git a/src/applications/audit/management/PhabricatorAuditManagementWorkflow.php b/src/applications/audit/management/PhabricatorAuditManagementWorkflow.php
index 6112a38e1..b9d90bddc 100644
--- a/src/applications/audit/management/PhabricatorAuditManagementWorkflow.php
+++ b/src/applications/audit/management/PhabricatorAuditManagementWorkflow.php
@@ -1,90 +1,125 @@
<?php
abstract class PhabricatorAuditManagementWorkflow
extends PhabricatorManagementWorkflow {
protected function getCommitConstraintArguments() {
return array(
array(
'name' => 'all',
'help' => pht('Update all commits in all repositories.'),
),
array(
'name' => 'objects',
'wildcard' => true,
'help' => pht('Update named commits and repositories.'),
),
);
}
protected function loadCommitsWithConstraints(PhutilArgumentParser $args) {
$viewer = $this->getViewer();
$all = $args->getArg('all');
$names = $args->getArg('objects');
if (!$names && !$all) {
throw new PhutilArgumentUsageException(
pht(
'Specify "--all" to affect everything, or a list of specific '.
'commits or repositories to affect.'));
} else if ($names && $all) {
throw new PhutilArgumentUsageException(
pht(
'Specify either a list of objects to affect or "--all", but not '.
'both.'));
}
if ($all) {
$objects = new LiskMigrationIterator(new PhabricatorRepository());
} else {
$query = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withNames($names);
$query->execute();
$objects = array();
$results = $query->getNamedResults();
foreach ($names as $name) {
if (!isset($results[$name])) {
throw new PhutilArgumentUsageException(
pht(
'Object "%s" is not a valid object.',
$name));
}
$object = $results[$name];
if (!($object instanceof PhabricatorRepository) &&
!($object instanceof PhabricatorRepositoryCommit)) {
throw new PhutilArgumentUsageException(
pht(
'Object "%s" is not a valid repository or commit.',
$name));
}
$objects[] = $object;
}
}
return $objects;
}
protected function loadCommitsForConstraintObject($object) {
$viewer = $this->getViewer();
if ($object instanceof PhabricatorRepository) {
$commits = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withRepository($object)
->execute();
} else {
$commits = array($object);
}
return $commits;
}
+ protected function synchronizeCommitAuditState($commit_phid) {
+ $viewer = $this->getViewer();
+
+ $commit = id(new DiffusionCommitQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($commit_phid))
+ ->needAuditRequests(true)
+ ->executeOne();
+ if (!$commit) {
+ return;
+ }
+
+ $old_status = $commit->getAuditStatusObject();
+ $commit->updateAuditStatus($commit->getAudits());
+ $new_status = $commit->getAuditStatusObject();
+
+ if ($old_status->getKey() == $new_status->getKey()) {
+ echo tsprintf(
+ "%s\n",
+ pht(
+ 'No synchronization changes for "%s".',
+ $commit->getDisplayName()));
+ } else {
+ echo tsprintf(
+ "%s\n",
+ pht(
+ 'Synchronizing "%s": "%s" -> "%s".',
+ $commit->getDisplayName(),
+ $old_status->getName(),
+ $new_status->getName()));
+
+ $commit->save();
+ }
+ }
+
}
diff --git a/src/applications/audit/management/PhabricatorAuditSynchronizeManagementWorkflow.php b/src/applications/audit/management/PhabricatorAuditSynchronizeManagementWorkflow.php
index 96d06e65c..abd0a3c63 100644
--- a/src/applications/audit/management/PhabricatorAuditSynchronizeManagementWorkflow.php
+++ b/src/applications/audit/management/PhabricatorAuditSynchronizeManagementWorkflow.php
@@ -1,58 +1,37 @@
<?php
final class PhabricatorAuditSynchronizeManagementWorkflow
extends PhabricatorAuditManagementWorkflow {
protected function didConstruct() {
$this
->setName('synchronize')
- ->setExamples('**synchronize** ...')
- ->setSynopsis(pht('Update audit status for commits.'))
+ ->setExamples(
+ "**synchronize** __repository__ ...\n".
+ "**synchronize** __commit__ ...\n".
+ "**synchronize** --all")
+ ->setSynopsis(
+ pht(
+ 'Update commits to make their summary audit state reflect the '.
+ 'state of their actual audit requests. This can fix inconsistencies '.
+ 'in database state if audit requests have been mangled '.
+ 'accidentally (or on purpose).'))
->setArguments(
array_merge(
$this->getCommitConstraintArguments(),
array()));
}
public function execute(PhutilArgumentParser $args) {
$viewer = $this->getViewer();
$objects = $this->loadCommitsWithConstraints($args);
foreach ($objects as $object) {
$commits = $this->loadCommitsForConstraintObject($object);
foreach ($commits as $commit) {
- $commit = id(new DiffusionCommitQuery())
- ->setViewer($viewer)
- ->withPHIDs(array($commit->getPHID()))
- ->needAuditRequests(true)
- ->executeOne();
- if (!$commit) {
- continue;
- }
-
- $old_status = $commit->getAuditStatusObject();
- $commit->updateAuditStatus($commit->getAudits());
- $new_status = $commit->getAuditStatusObject();
-
- if ($old_status->getKey() == $new_status->getKey()) {
- echo tsprintf(
- "%s\n",
- pht(
- 'No changes for "%s".',
- $commit->getDisplayName()));
- } else {
- echo tsprintf(
- "%s\n",
- pht(
- 'Updating "%s": "%s" -> "%s".',
- $commit->getDisplayName(),
- $old_status->getName(),
- $new_status->getName()));
-
- $commit->save();
- }
+ $this->synchronizeCommitAuditState($commit->getPHID());
}
}
}
}
diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php
index df86595b4..3446ad597 100644
--- a/src/applications/auth/application/PhabricatorAuthApplication.php
+++ b/src/applications/auth/application/PhabricatorAuthApplication.php
@@ -1,154 +1,158 @@
<?php
final class PhabricatorAuthApplication extends PhabricatorApplication {
public function canUninstall() {
return false;
}
public function getBaseURI() {
return '/auth/';
}
public function getIcon() {
return 'fa-key';
}
public function isPinnedByDefault(PhabricatorUser $viewer) {
return $viewer->getIsAdmin();
}
public function getName() {
return pht('Auth');
}
public function getShortDescription() {
return pht('Login/Registration');
}
public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
// NOTE: Although reasonable help exists for this in "Configuring Accounts
// and Registration", specifying help items here means we get the menu
// item in all the login/link interfaces, which is confusing and not
// helpful.
// TODO: Special case this, or split the auth and auth administration
// applications?
return array();
}
public function getApplicationGroup() {
return self::GROUP_ADMIN;
}
public function getRoutes() {
return array(
'/auth/' => array(
'' => 'PhabricatorAuthListController',
'config/' => array(
'new/' => 'PhabricatorAuthNewController',
- 'new/(?P<className>[^/]+)/' => 'PhabricatorAuthEditController',
- 'edit/(?P<id>\d+)/' => 'PhabricatorAuthEditController',
+ 'edit/(?:(?P<id>\d+)/)?' => 'PhabricatorAuthEditController',
'(?P<action>enable|disable)/(?P<id>\d+)/'
=> 'PhabricatorAuthDisableController',
+ 'view/(?P<id>\d+)/' => 'PhabricatorAuthProviderViewController',
),
'login/(?P<pkey>[^/]+)/(?:(?P<extra>[^/]+)/)?'
=> 'PhabricatorAuthLoginController',
'(?P<loggedout>loggedout)/' => 'PhabricatorAuthStartController',
'invite/(?P<code>[^/]+)/' => 'PhabricatorAuthInviteController',
'register/(?:(?P<akey>[^/]+)/)?' => 'PhabricatorAuthRegisterController',
'start/' => 'PhabricatorAuthStartController',
'validate/' => 'PhabricatorAuthValidateController',
'finish/' => 'PhabricatorAuthFinishController',
- 'unlink/(?P<pkey>[^/]+)/' => 'PhabricatorAuthUnlinkController',
- '(?P<action>link|refresh)/(?P<pkey>[^/]+)/'
+ 'unlink/(?P<id>\d+)/' => 'PhabricatorAuthUnlinkController',
+ '(?P<action>link|refresh)/(?P<id>\d+)/'
=> 'PhabricatorAuthLinkController',
'confirmlink/(?P<akey>[^/]+)/'
=> 'PhabricatorAuthConfirmLinkController',
'session/terminate/(?P<id>[^/]+)/'
=> 'PhabricatorAuthTerminateSessionController',
'token/revoke/(?P<id>[^/]+)/'
=> 'PhabricatorAuthRevokeTokenController',
'session/downgrade/'
=> 'PhabricatorAuthDowngradeSessionController',
'enroll/' => array(
'(?:(?P<pageKey>[^/]+)/)?(?:(?P<formSaved>saved)/)?'
=> 'PhabricatorAuthNeedsMultiFactorController',
),
'sshkey/' => array(
$this->getQueryRoutePattern('for/(?P<forPHID>[^/]+)/')
=> 'PhabricatorAuthSSHKeyListController',
'generate/' => 'PhabricatorAuthSSHKeyGenerateController',
'upload/' => 'PhabricatorAuthSSHKeyEditController',
'edit/(?P<id>\d+)/' => 'PhabricatorAuthSSHKeyEditController',
'revoke/(?P<id>\d+)/'
=> 'PhabricatorAuthSSHKeyRevokeController',
'view/(?P<id>\d+)/' => 'PhabricatorAuthSSHKeyViewController',
),
+
'password/' => 'PhabricatorAuthSetPasswordController',
+ 'external/' => 'PhabricatorAuthSetExternalController',
'mfa/' => array(
$this->getQueryRoutePattern() =>
'PhabricatorAuthFactorProviderListController',
$this->getEditRoutePattern('edit/') =>
'PhabricatorAuthFactorProviderEditController',
'(?P<id>[1-9]\d*)/' =>
'PhabricatorAuthFactorProviderViewController',
'message/(?P<id>[1-9]\d*)/' =>
'PhabricatorAuthFactorProviderMessageController',
+ 'challenge/status/(?P<id>[1-9]\d*)/' =>
+ 'PhabricatorAuthChallengeStatusController',
),
'message/' => array(
$this->getQueryRoutePattern() =>
'PhabricatorAuthMessageListController',
$this->getEditRoutePattern('edit/') =>
'PhabricatorAuthMessageEditController',
'(?P<id>[1-9]\d*)/' =>
'PhabricatorAuthMessageViewController',
),
'contact/' => array(
$this->getEditRoutePattern('edit/') =>
'PhabricatorAuthContactNumberEditController',
'(?P<id>[1-9]\d*)/' =>
'PhabricatorAuthContactNumberViewController',
'(?P<action>disable|enable)/(?P<id>[1-9]\d*)/' =>
'PhabricatorAuthContactNumberDisableController',
'primary/(?P<id>[1-9]\d*)/' =>
'PhabricatorAuthContactNumberPrimaryController',
'test/(?P<id>[1-9]\d*)/' =>
'PhabricatorAuthContactNumberTestController',
),
),
'/oauth/(?P<provider>\w+)/login/'
=> 'PhabricatorAuthOldOAuthRedirectController',
'/login/' => array(
'' => 'PhabricatorAuthStartController',
'email/' => 'PhabricatorEmailLoginController',
'once/'.
'(?P<type>[^/]+)/'.
'(?P<id>\d+)/'.
'(?P<key>[^/]+)/'.
'(?:(?P<emailID>\d+)/)?' => 'PhabricatorAuthOneTimeLoginController',
'refresh/' => 'PhabricatorRefreshCSRFController',
'mustverify/' => 'PhabricatorMustVerifyEmailController',
),
'/emailverify/(?P<code>[^/]+)/'
=> 'PhabricatorEmailVerificationController',
'/logout/' => 'PhabricatorLogoutController',
);
}
protected function getCustomCapabilities() {
return array(
AuthManageProvidersCapability::CAPABILITY => array(
'default' => PhabricatorPolicies::POLICY_ADMIN,
),
);
}
}
diff --git a/src/applications/auth/controller/PhabricatorAuthConfirmLinkController.php b/src/applications/auth/controller/PhabricatorAuthConfirmLinkController.php
index 664a97885..9ceb10df8 100644
--- a/src/applications/auth/controller/PhabricatorAuthConfirmLinkController.php
+++ b/src/applications/auth/controller/PhabricatorAuthConfirmLinkController.php
@@ -1,78 +1,79 @@
<?php
final class PhabricatorAuthConfirmLinkController
extends PhabricatorAuthController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$accountkey = $request->getURIData('akey');
$result = $this->loadAccountForRegistrationOrLinking($accountkey);
list($account, $provider, $response) = $result;
if ($response) {
return $response;
}
if (!$provider->shouldAllowAccountLink()) {
return $this->renderError(pht('This account is not linkable.'));
}
$panel_uri = '/settings/panel/external/';
- if ($request->isFormPost()) {
+ if ($request->isFormOrHisecPost()) {
+ $workflow_key = sprintf(
+ 'account.link(%s)',
+ $account->getPHID());
+
+ $hisec_token = id(new PhabricatorAuthSessionEngine())
+ ->setWorkflowKey($workflow_key)
+ ->requireHighSecurityToken($viewer, $request, $panel_uri);
+
$account->setUserPHID($viewer->getPHID());
$account->save();
$this->clearRegistrationCookies();
// TODO: Send the user email about the new account link.
return id(new AphrontRedirectResponse())->setURI($panel_uri);
}
- // TODO: Provide more information about the external account. Clicking
- // through this form blindly is dangerous.
-
- // TODO: If the user has password authentication, require them to retype
- // their password here.
-
- $dialog = id(new AphrontDialogView())
- ->setUser($viewer)
+ $dialog = $this->newDialog()
->setTitle(pht('Confirm %s Account Link', $provider->getProviderName()))
->addCancelButton($panel_uri)
->addSubmitButton(pht('Confirm Account Link'));
$form = id(new PHUIFormLayoutView())
->setFullWidth(true)
->appendChild(
phutil_tag(
'div',
array(
'class' => 'aphront-form-instructions',
),
pht(
'Confirm the link with this %s account. This account will be '.
'able to log in to your Phabricator account.',
$provider->getProviderName())))
->appendChild(
id(new PhabricatorAuthAccountView())
->setUser($viewer)
->setExternalAccount($account)
->setAuthProvider($provider));
$dialog->appendChild($form);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Confirm Link'), $panel_uri);
$crumbs->addTextCrumb($provider->getProviderName());
$crumbs->setBorder(true);
return $this->newPage()
->setTitle(pht('Confirm External Account Link'))
->setCrumbs($crumbs)
->appendChild($dialog);
}
}
diff --git a/src/applications/auth/controller/PhabricatorAuthController.php b/src/applications/auth/controller/PhabricatorAuthController.php
index 9b7267ec9..cda56d34b 100644
--- a/src/applications/auth/controller/PhabricatorAuthController.php
+++ b/src/applications/auth/controller/PhabricatorAuthController.php
@@ -1,289 +1,289 @@
<?php
abstract class PhabricatorAuthController extends PhabricatorController {
protected function renderErrorPage($title, array $messages) {
$view = new PHUIInfoView();
$view->setTitle($title);
$view->setErrors($messages);
return $this->newPage()
->setTitle($title)
->appendChild($view);
}
/**
* Returns true if this install is newly setup (i.e., there are no user
* accounts yet). In this case, we enter a special mode to permit creation
* of the first account form the web UI.
*/
protected function isFirstTimeSetup() {
// If there are any auth providers, this isn't first time setup, even if
// we don't have accounts.
if (PhabricatorAuthProvider::getAllEnabledProviders()) {
return false;
}
// Otherwise, check if there are any user accounts. If not, we're in first
// time setup.
$any_users = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->setLimit(1)
->execute();
return !$any_users;
}
/**
* Log a user into a web session and return an @{class:AphrontResponse} which
* corresponds to continuing the login process.
*
* Normally, this is a redirect to the validation controller which makes sure
* the user's cookies are set. However, event listeners can intercept this
* event and do something else if they prefer.
*
* @param PhabricatorUser User to log the viewer in as.
* @param bool True to issue a full session immediately, bypassing MFA.
* @return AphrontResponse Response which continues the login process.
*/
protected function loginUser(
PhabricatorUser $user,
$force_full_session = false) {
$response = $this->buildLoginValidateResponse($user);
$session_type = PhabricatorAuthSession::TYPE_WEB;
if ($force_full_session) {
$partial_session = false;
} else {
$partial_session = true;
}
$session_key = id(new PhabricatorAuthSessionEngine())
->establishSession($session_type, $user->getPHID(), $partial_session);
// NOTE: We allow disabled users to login and roadblock them later, so
// there's no check for users being disabled here.
$request = $this->getRequest();
$request->setCookie(
PhabricatorCookies::COOKIE_USERNAME,
$user->getUsername());
$request->setCookie(
PhabricatorCookies::COOKIE_SESSION,
$session_key);
$this->clearRegistrationCookies();
return $response;
}
protected function clearRegistrationCookies() {
$request = $this->getRequest();
// Clear the registration key.
$request->clearCookie(PhabricatorCookies::COOKIE_REGISTRATION);
// Clear the client ID / OAuth state key.
$request->clearCookie(PhabricatorCookies::COOKIE_CLIENTID);
// Clear the invite cookie.
$request->clearCookie(PhabricatorCookies::COOKIE_INVITE);
}
private function buildLoginValidateResponse(PhabricatorUser $user) {
$validate_uri = new PhutilURI($this->getApplicationURI('validate/'));
- $validate_uri->setQueryParam('expect', $user->getUsername());
+ $validate_uri->replaceQueryParam('expect', $user->getUsername());
return id(new AphrontRedirectResponse())->setURI((string)$validate_uri);
}
protected function renderError($message) {
return $this->renderErrorPage(
pht('Authentication Error'),
array(
$message,
));
}
protected function loadAccountForRegistrationOrLinking($account_key) {
$request = $this->getRequest();
$viewer = $request->getUser();
$account = null;
$provider = null;
$response = null;
if (!$account_key) {
$response = $this->renderError(
pht('Request did not include account key.'));
return array($account, $provider, $response);
}
// NOTE: We're using the omnipotent user because the actual user may not
// be logged in yet, and because we want to tailor an error message to
// distinguish between "not usable" and "does not exist". We do explicit
// checks later on to make sure this account is valid for the intended
// operation. This requires edit permission for completeness and consistency
// but it won't actually be meaningfully checked because we're using the
// omnipotent user.
$account = id(new PhabricatorExternalAccountQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withAccountSecrets(array($account_key))
->needImages(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$account) {
$response = $this->renderError(pht('No valid linkable account.'));
return array($account, $provider, $response);
}
if ($account->getUserPHID()) {
if ($account->getUserPHID() != $viewer->getPHID()) {
$response = $this->renderError(
pht(
'The account you are attempting to register or link is already '.
'linked to another user.'));
} else {
$response = $this->renderError(
pht(
'The account you are attempting to link is already linked '.
'to your account.'));
}
return array($account, $provider, $response);
}
$registration_key = $request->getCookie(
PhabricatorCookies::COOKIE_REGISTRATION);
// NOTE: This registration key check is not strictly necessary, because
// we're only creating new accounts, not linking existing accounts. It
// might be more hassle than it is worth, especially for email.
//
// The attack this prevents is getting to the registration screen, then
// copy/pasting the URL and getting someone else to click it and complete
// the process. They end up with an account bound to credentials you
// control. This doesn't really let you do anything meaningful, though,
// since you could have simply completed the process yourself.
if (!$registration_key) {
$response = $this->renderError(
pht(
'Your browser did not submit a registration key with the request. '.
'You must use the same browser to begin and complete registration. '.
'Check that cookies are enabled and try again.'));
return array($account, $provider, $response);
}
// We store the digest of the key rather than the key itself to prevent a
// theoretical attacker with read-only access to the database from
// hijacking registration sessions.
$actual = $account->getProperty('registrationKey');
$expect = PhabricatorHash::weakDigest($registration_key);
if (!phutil_hashes_are_identical($actual, $expect)) {
$response = $this->renderError(
pht(
'Your browser submitted a different registration key than the one '.
'associated with this account. You may need to clear your cookies.'));
return array($account, $provider, $response);
}
$other_account = id(new PhabricatorExternalAccount())->loadAllWhere(
'accountType = %s AND accountDomain = %s AND accountID = %s
AND id != %d',
$account->getAccountType(),
$account->getAccountDomain(),
$account->getAccountID(),
$account->getID());
if ($other_account) {
$response = $this->renderError(
pht(
'The account you are attempting to register with already belongs '.
'to another user.'));
return array($account, $provider, $response);
}
- $provider = PhabricatorAuthProvider::getEnabledProviderByKey(
- $account->getProviderKey());
-
- if (!$provider) {
+ $config = $account->getProviderConfig();
+ if (!$config->getIsEnabled()) {
$response = $this->renderError(
pht(
- 'The account you are attempting to register with uses a nonexistent '.
- 'or disabled authentication provider (with key "%s"). An '.
- 'administrator may have recently disabled this provider.',
- $account->getProviderKey()));
+ 'The account you are attempting to register with uses a disabled '.
+ 'authentication provider ("%s"). An administrator may have '.
+ 'recently disabled this provider.',
+ $config->getDisplayName()));
return array($account, $provider, $response);
}
+ $provider = $config->getProvider();
+
return array($account, $provider, null);
}
protected function loadInvite() {
$invite_cookie = PhabricatorCookies::COOKIE_INVITE;
$invite_code = $this->getRequest()->getCookie($invite_cookie);
if (!$invite_code) {
return null;
}
$engine = id(new PhabricatorAuthInviteEngine())
->setViewer($this->getViewer())
->setUserHasConfirmedVerify(true);
try {
return $engine->processInviteCode($invite_code);
} catch (Exception $ex) {
// If this fails for any reason, just drop the invite. In normal
// circumstances, we gave them a detailed explanation of any error
// before they jumped into this workflow.
return null;
}
}
protected function renderInviteHeader(PhabricatorAuthInvite $invite) {
$viewer = $this->getViewer();
// Since the user hasn't registered yet, they may not be able to see other
// user accounts. Load the inviting user with the omnipotent viewer.
$omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
$invite_author = id(new PhabricatorPeopleQuery())
->setViewer($omnipotent_viewer)
->withPHIDs(array($invite->getAuthorPHID()))
->needProfileImage(true)
->executeOne();
// If we can't load the author for some reason, just drop this message.
// We lose the value of contextualizing things without author details.
if (!$invite_author) {
return null;
}
$invite_item = id(new PHUIObjectItemView())
->setHeader(pht('Welcome to Phabricator!'))
->setImageURI($invite_author->getProfileImageURI())
->addAttribute(
pht(
'%s has invited you to join Phabricator.',
$invite_author->getFullName()));
$invite_list = id(new PHUIObjectItemListView())
->addItem($invite_item)
->setFlush(true);
return id(new PHUIBoxView())
->addMargin(PHUI::MARGIN_LARGE)
->appendChild($invite_list);
}
}
diff --git a/src/applications/auth/controller/PhabricatorAuthLinkController.php b/src/applications/auth/controller/PhabricatorAuthLinkController.php
index 44176a278..4b127b9ad 100644
--- a/src/applications/auth/controller/PhabricatorAuthLinkController.php
+++ b/src/applications/auth/controller/PhabricatorAuthLinkController.php
@@ -1,127 +1,128 @@
<?php
final class PhabricatorAuthLinkController
extends PhabricatorAuthController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$action = $request->getURIData('action');
- $provider_key = $request->getURIData('pkey');
- $provider = PhabricatorAuthProvider::getEnabledProviderByKey(
- $provider_key);
- if (!$provider) {
+ $id = $request->getURIData('id');
+
+ $config = id(new PhabricatorAuthProviderConfigQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($id))
+ ->withIsEnabled(true)
+ ->executeOne();
+ if (!$config) {
return new Aphront404Response();
}
+ $provider = $config->getProvider();
+
switch ($action) {
case 'link':
if (!$provider->shouldAllowAccountLink()) {
return $this->renderErrorPage(
pht('Account Not Linkable'),
array(
pht('This provider is not configured to allow linking.'),
));
}
break;
case 'refresh':
if (!$provider->shouldAllowAccountRefresh()) {
return $this->renderErrorPage(
pht('Account Not Refreshable'),
array(
pht('This provider does not allow refreshing.'),
));
}
break;
default:
return new Aphront400Response();
}
- $account = id(new PhabricatorExternalAccount())->loadOneWhere(
- 'accountType = %s AND accountDomain = %s AND userPHID = %s',
- $provider->getProviderType(),
- $provider->getProviderDomain(),
- $viewer->getPHID());
+ $accounts = id(new PhabricatorExternalAccountQuery())
+ ->setViewer($viewer)
+ ->withUserPHIDs(array($viewer->getPHID()))
+ ->withProviderConfigPHIDs(array($config->getPHID()))
+ ->execute();
switch ($action) {
case 'link':
- if ($account) {
+ if ($accounts) {
return $this->renderErrorPage(
pht('Account Already Linked'),
array(
pht(
'Your Phabricator account is already linked to an external '.
'account for this provider.'),
));
}
break;
case 'refresh':
- if (!$account) {
+ if (!$accounts) {
return $this->renderErrorPage(
pht('No Account Linked'),
array(
pht(
'You do not have a linked account on this provider, and thus '.
'can not refresh it.'),
));
}
break;
default:
return new Aphront400Response();
}
$panel_uri = '/settings/panel/external/';
PhabricatorCookies::setClientIDCookie($request);
switch ($action) {
case 'link':
- id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
- $viewer,
- $request,
- $panel_uri);
-
$form = $provider->buildLinkForm($this);
break;
case 'refresh':
$form = $provider->buildRefreshForm($this);
break;
default:
return new Aphront400Response();
}
if ($provider->isLoginFormAButton()) {
require_celerity_resource('auth-css');
$form = phutil_tag(
'div',
array(
'class' => 'phabricator-link-button pl',
),
$form);
}
switch ($action) {
case 'link':
$name = pht('Link Account');
$title = pht('Link %s Account', $provider->getProviderName());
break;
case 'refresh':
$name = pht('Refresh Account');
$title = pht('Refresh %s Account', $provider->getProviderName());
break;
default:
return new Aphront400Response();
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Link Account'), $panel_uri);
$crumbs->addTextCrumb($provider->getProviderName($name));
$crumbs->setBorder(true);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($form);
}
}
diff --git a/src/applications/auth/controller/PhabricatorAuthLoginController.php b/src/applications/auth/controller/PhabricatorAuthLoginController.php
index 54649a6a6..e7dabd934 100644
--- a/src/applications/auth/controller/PhabricatorAuthLoginController.php
+++ b/src/applications/auth/controller/PhabricatorAuthLoginController.php
@@ -1,267 +1,268 @@
<?php
final class PhabricatorAuthLoginController
extends PhabricatorAuthController {
private $providerKey;
private $extraURIData;
private $provider;
public function shouldRequireLogin() {
return false;
}
public function shouldAllowRestrictedParameter($parameter_name) {
// Whitelist the OAuth 'code' parameter.
if ($parameter_name == 'code') {
return true;
}
return parent::shouldAllowRestrictedParameter($parameter_name);
}
public function getExtraURIData() {
return $this->extraURIData;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$this->providerKey = $request->getURIData('pkey');
$this->extraURIData = $request->getURIData('extra');
$response = $this->loadProvider();
if ($response) {
return $response;
}
+ $invite = $this->loadInvite();
$provider = $this->provider;
try {
list($account, $response) = $provider->processLoginRequest($this);
} catch (PhutilAuthUserAbortedException $ex) {
if ($viewer->isLoggedIn()) {
// If a logged-in user cancels, take them back to the external accounts
// panel.
$next_uri = '/settings/panel/external/';
} else {
// If a logged-out user cancels, take them back to the auth start page.
$next_uri = '/';
}
// User explicitly hit "Cancel".
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('Authentication Canceled'))
->appendChild(
pht('You canceled authentication.'))
->addCancelButton($next_uri, pht('Continue'));
return id(new AphrontDialogResponse())->setDialog($dialog);
}
if ($response) {
return $response;
}
if (!$account) {
throw new Exception(
pht(
'Auth provider failed to load an account from %s!',
'processLoginRequest()'));
}
if ($account->getUserPHID()) {
// The account is already attached to a Phabricator user, so this is
// either a login or a bad account link request.
if (!$viewer->isLoggedIn()) {
if ($provider->shouldAllowLogin()) {
return $this->processLoginUser($account);
} else {
return $this->renderError(
pht(
'The external account ("%s") you just authenticated with is '.
'not configured to allow logins on this Phabricator install. '.
'An administrator may have recently disabled it.',
$provider->getProviderName()));
}
} else if ($viewer->getPHID() == $account->getUserPHID()) {
// This is either an attempt to re-link an existing and already
// linked account (which is silly) or a refresh of an external account
// (e.g., an OAuth account).
return id(new AphrontRedirectResponse())
->setURI('/settings/panel/external/');
} else {
return $this->renderError(
pht(
'The external account ("%s") you just used to log in is already '.
'associated with another Phabricator user account. Log in to the '.
'other Phabricator account and unlink the external account before '.
'linking it to a new Phabricator account.',
$provider->getProviderName()));
}
} else {
// The account is not yet attached to a Phabricator user, so this is
// either a registration or an account link request.
if (!$viewer->isLoggedIn()) {
- if ($provider->shouldAllowRegistration()) {
+ if ($provider->shouldAllowRegistration() || $invite) {
return $this->processRegisterUser($account);
} else {
return $this->renderError(
pht(
'The external account ("%s") you just authenticated with is '.
'not configured to allow registration on this Phabricator '.
'install. An administrator may have recently disabled it.',
$provider->getProviderName()));
}
} else {
// If the user already has a linked account of this type, prevent them
// from linking a second account. This can happen if they swap logins
// and then refresh the account link. See T6707. We will eventually
// allow this after T2549.
$existing_accounts = id(new PhabricatorExternalAccountQuery())
->setViewer($viewer)
->withUserPHIDs(array($viewer->getPHID()))
->withAccountTypes(array($account->getAccountType()))
->execute();
if ($existing_accounts) {
return $this->renderError(
pht(
'Your Phabricator account is already connected to an external '.
'account on this provider ("%s"), but you are currently logged '.
'in to the provider with a different account. Log out of the '.
'external service, then log back in with the correct account '.
'before refreshing the account link.',
$provider->getProviderName()));
}
if ($provider->shouldAllowAccountLink()) {
return $this->processLinkUser($account);
} else {
return $this->renderError(
pht(
'The external account ("%s") you just authenticated with is '.
'not configured to allow account linking on this Phabricator '.
'install. An administrator may have recently disabled it.',
$provider->getProviderName()));
}
}
}
// This should be unreachable, but fail explicitly if we get here somehow.
return new Aphront400Response();
}
private function processLoginUser(PhabricatorExternalAccount $account) {
$user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$account->getUserPHID());
if (!$user) {
return $this->renderError(
pht(
'The external account you just logged in with is not associated '.
'with a valid Phabricator user.'));
}
return $this->loginUser($user);
}
private function processRegisterUser(PhabricatorExternalAccount $account) {
$account_secret = $account->getAccountSecret();
$register_uri = $this->getApplicationURI('register/'.$account_secret.'/');
return $this->setAccountKeyAndContinue($account, $register_uri);
}
private function processLinkUser(PhabricatorExternalAccount $account) {
$account_secret = $account->getAccountSecret();
$confirm_uri = $this->getApplicationURI('confirmlink/'.$account_secret.'/');
return $this->setAccountKeyAndContinue($account, $confirm_uri);
}
private function setAccountKeyAndContinue(
PhabricatorExternalAccount $account,
$next_uri) {
if ($account->getUserPHID()) {
throw new Exception(pht('Account is already registered or linked.'));
}
// Regenerate the registration secret key, set it on the external account,
// set a cookie on the user's machine, and redirect them to registration.
// See PhabricatorAuthRegisterController for discussion of the registration
// key.
$registration_key = Filesystem::readRandomCharacters(32);
$account->setProperty(
'registrationKey',
PhabricatorHash::weakDigest($registration_key));
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$account->save();
unset($unguarded);
$this->getRequest()->setTemporaryCookie(
PhabricatorCookies::COOKIE_REGISTRATION,
$registration_key);
return id(new AphrontRedirectResponse())->setURI($next_uri);
}
private function loadProvider() {
$provider = PhabricatorAuthProvider::getEnabledProviderByKey(
$this->providerKey);
if (!$provider) {
return $this->renderError(
pht(
'The account you are attempting to log in with uses a nonexistent '.
'or disabled authentication provider (with key "%s"). An '.
'administrator may have recently disabled this provider.',
$this->providerKey));
}
$this->provider = $provider;
return null;
}
protected function renderError($message) {
return $this->renderErrorPage(
pht('Login Failed'),
array($message));
}
public function buildProviderPageResponse(
PhabricatorAuthProvider $provider,
$content) {
$crumbs = $this->buildApplicationCrumbs();
if ($this->getRequest()->getUser()->isLoggedIn()) {
$crumbs->addTextCrumb(pht('Link Account'), $provider->getSettingsURI());
} else {
$crumbs->addTextCrumb(pht('Log In'), $this->getApplicationURI('start/'));
}
$crumbs->addTextCrumb($provider->getProviderName());
$crumbs->setBorder(true);
return $this->newPage()
->setTitle(pht('Log In'))
->setCrumbs($crumbs)
->appendChild($content);
}
public function buildProviderErrorResponse(
PhabricatorAuthProvider $provider,
$message) {
$message = pht(
'Authentication provider ("%s") encountered an error while attempting '.
'to log in. %s', $provider->getProviderName(), $message);
return $this->renderError($message);
}
}
diff --git a/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php b/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php
index 0cac95f53..d176a6711 100644
--- a/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php
+++ b/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php
@@ -1,209 +1,275 @@
<?php
final class PhabricatorAuthOneTimeLoginController
extends PhabricatorAuthController {
public function shouldRequireLogin() {
return false;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$link_type = $request->getURIData('type');
$key = $request->getURIData('key');
$email_id = $request->getURIData('emailID');
- if ($request->getUser()->isLoggedIn()) {
- return $this->renderError(
- pht('You are already logged in.'));
- }
-
$target_user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs(array($id))
->executeOne();
if (!$target_user) {
return new Aphront404Response();
}
+ // NOTE: We allow you to use a one-time login link for your own current
+ // login account. This supports the "Set Password" flow.
+
+ $is_logged_in = false;
+ if ($viewer->isLoggedIn()) {
+ if ($viewer->getPHID() !== $target_user->getPHID()) {
+ return $this->renderError(
+ pht('You are already logged in.'));
+ } else {
+ $is_logged_in = true;
+ }
+ }
+
// NOTE: As a convenience to users, these one-time login URIs may also
// be associated with an email address which will be verified when the
// URI is used.
// This improves the new user experience for users receiving "Welcome"
// emails on installs that require verification: if we did not verify the
// email, they'd immediately get roadblocked with a "Verify Your Email"
// error and have to go back to their email account, wait for a
// "Verification" email, and then click that link to actually get access to
// their account. This is hugely unwieldy, and if the link was only sent
// to the user's email in the first place we can safely verify it as a
// side effect of login.
// The email hashed into the URI so users can't verify some email they
// do not own by doing this:
//
// - Add some address you do not own;
// - request a password reset;
// - change the URI in the email to the address you don't own;
// - login via the email link; and
// - get a "verified" address you don't control.
$target_email = null;
if ($email_id) {
$target_email = id(new PhabricatorUserEmail())->loadOneWhere(
'userPHID = %s AND id = %d',
$target_user->getPHID(),
$email_id);
if (!$target_email) {
return new Aphront404Response();
}
}
$engine = new PhabricatorAuthSessionEngine();
$token = $engine->loadOneTimeLoginKey(
$target_user,
$target_email,
$key);
if (!$token) {
return $this->newDialog()
->setTitle(pht('Unable to Log In'))
->setShortTitle(pht('Login Failure'))
->appendParagraph(
pht(
'The login link you clicked is invalid, out of date, or has '.
'already been used.'))
->appendParagraph(
pht(
'Make sure you are copy-and-pasting the entire link into '.
'your browser. Login links are only valid for 24 hours, and '.
'can only be used once.'))
->appendParagraph(
pht('You can try again, or request a new link via email.'))
->addCancelButton('/login/email/', pht('Send Another Email'));
}
if (!$target_user->canEstablishWebSessions()) {
return $this->newDialog()
->setTitle(pht('Unable to Establish Web Session'))
->setShortTitle(pht('Login Failure'))
->appendParagraph(
pht(
'You are trying to gain access to an account ("%s") that can not '.
'establish a web session.',
$target_user->getUsername()))
->appendParagraph(
pht(
'Special users like daemons and mailing lists are not permitted '.
'to log in via the web. Log in as a normal user instead.'))
->addCancelButton('/');
}
- if ($request->isFormPost()) {
+ if ($request->isFormPost() || $is_logged_in) {
// If we have an email bound into this URI, verify email so that clicking
// the link in the "Welcome" email is good enough, without requiring users
// to go through a second round of email verification.
$editor = id(new PhabricatorUserEditor())
->setActor($target_user);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
// Nuke the token and all other outstanding password reset tokens.
// There is no particular security benefit to destroying them all, but
// it should reduce HackerOne reports of nebulous harm.
$editor->revokePasswordResetLinks($target_user);
if ($target_email) {
$editor->verifyEmail($target_user, $target_email);
}
unset($unguarded);
- $next = '/';
- if (!PhabricatorPasswordAuthProvider::getPasswordProvider()) {
- $next = '/settings/panel/external/';
- } else {
+ $next_uri = $this->getNextStepURI($target_user);
- // We're going to let the user reset their password without knowing
- // the old one. Generate a one-time token for that.
- $key = Filesystem::readRandomCharacters(16);
- $password_type =
- PhabricatorAuthPasswordResetTemporaryTokenType::TOKENTYPE;
-
- $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
- id(new PhabricatorAuthTemporaryToken())
- ->setTokenResource($target_user->getPHID())
- ->setTokenType($password_type)
- ->setTokenExpires(time() + phutil_units('1 hour in seconds'))
- ->setTokenCode(PhabricatorHash::weakDigest($key))
- ->save();
- unset($unguarded);
-
- $panel_uri = '/auth/password/';
-
- $next = (string)id(new PhutilURI($panel_uri))
- ->setQueryParams(
- array(
- 'key' => $key,
- ));
-
- $request->setTemporaryCookie(PhabricatorCookies::COOKIE_HISEC, 'yes');
+ // If the user is already logged in, we're just doing a "password set"
+ // flow. Skip directly to the next step.
+ if ($is_logged_in) {
+ return id(new AphrontRedirectResponse())->setURI($next_uri);
}
- PhabricatorCookies::setNextURICookie($request, $next, $force = true);
+ PhabricatorCookies::setNextURICookie($request, $next_uri, $force = true);
$force_full_session = false;
if ($link_type === PhabricatorAuthSessionEngine::ONETIME_RECOVER) {
$force_full_session = $token->getShouldForceFullSession();
}
return $this->loginUser($target_user, $force_full_session);
}
// NOTE: We need to CSRF here so attackers can't generate an email link,
// then log a user in to an account they control via sneaky invisible
// form submissions.
switch ($link_type) {
case PhabricatorAuthSessionEngine::ONETIME_WELCOME:
$title = pht('Welcome to Phabricator');
break;
case PhabricatorAuthSessionEngine::ONETIME_RECOVER:
$title = pht('Account Recovery');
break;
case PhabricatorAuthSessionEngine::ONETIME_USERNAME:
case PhabricatorAuthSessionEngine::ONETIME_RESET:
default:
$title = pht('Log in to Phabricator');
break;
}
$body = array();
$body[] = pht(
'Use the button below to log in as: %s',
phutil_tag('strong', array(), $target_user->getUsername()));
if ($target_email && !$target_email->getIsVerified()) {
$body[] = pht(
'Logging in will verify %s as an email address you own.',
phutil_tag('strong', array(), $target_email->getAddress()));
}
$body[] = pht(
'After logging in you should set a password for your account, or '.
'link your account to an external account that you can use to '.
'authenticate in the future.');
$dialog = $this->newDialog()
->setTitle($title)
->addSubmitButton(pht('Log In (%s)', $target_user->getUsername()))
->addCancelButton('/');
foreach ($body as $paragraph) {
$dialog->appendParagraph($paragraph);
}
return id(new AphrontDialogResponse())->setDialog($dialog);
}
+
+ private function getNextStepURI(PhabricatorUser $user) {
+ $request = $this->getRequest();
+
+ // If we have password auth, let the user set or reset their password after
+ // login.
+ $have_passwords = PhabricatorPasswordAuthProvider::getPasswordProvider();
+ if ($have_passwords) {
+ // We're going to let the user reset their password without knowing
+ // the old one. Generate a one-time token for that.
+ $key = Filesystem::readRandomCharacters(16);
+ $password_type =
+ PhabricatorAuthPasswordResetTemporaryTokenType::TOKENTYPE;
+
+ $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
+ id(new PhabricatorAuthTemporaryToken())
+ ->setTokenResource($user->getPHID())
+ ->setTokenType($password_type)
+ ->setTokenExpires(time() + phutil_units('1 hour in seconds'))
+ ->setTokenCode(PhabricatorHash::weakDigest($key))
+ ->save();
+ unset($unguarded);
+
+ $panel_uri = '/auth/password/';
+
+ $request->setTemporaryCookie(PhabricatorCookies::COOKIE_HISEC, 'yes');
+
+ $params = array(
+ 'key' => $key,
+ );
+
+ return (string)new PhutilURI($panel_uri, $params);
+ }
+
+ // Check if the user already has external accounts linked. If they do,
+ // it's not obvious why they aren't using them to log in, but assume they
+ // know what they're doing. We won't send them to the link workflow.
+ $accounts = id(new PhabricatorExternalAccountQuery())
+ ->setViewer($user)
+ ->withUserPHIDs(array($user->getPHID()))
+ ->execute();
+
+ $configs = id(new PhabricatorAuthProviderConfigQuery())
+ ->setViewer($user)
+ ->withIsEnabled(true)
+ ->execute();
+
+ $linkable = array();
+ foreach ($configs as $config) {
+ if (!$config->getShouldAllowLink()) {
+ continue;
+ }
+
+ $provider = $config->getProvider();
+ if (!$provider->isLoginFormAButton()) {
+ continue;
+ }
+
+ $linkable[] = $provider;
+ }
+
+ // If there's at least one linkable provider, and the user doesn't already
+ // have accounts, send the user to the link workflow.
+ if (!$accounts && $linkable) {
+ return '/auth/external/';
+ }
+
+ // If there are no configured providers and the user is an administrator,
+ // send them to Auth to configure a provider. This is probably where they
+ // want to go. You can end up in this state by accidentally losing your
+ // first session during initial setup, or after restoring exported data
+ // from a hosted instance.
+ if (!$configs && $user->getIsAdmin()) {
+ return '/auth/';
+ }
+
+ // If we didn't find anywhere better to send them, give up and just send
+ // them to the home page.
+ return '/';
+ }
+
}
diff --git a/src/applications/auth/controller/PhabricatorAuthRegisterController.php b/src/applications/auth/controller/PhabricatorAuthRegisterController.php
index 9e1aef592..5a46d0e60 100644
--- a/src/applications/auth/controller/PhabricatorAuthRegisterController.php
+++ b/src/applications/auth/controller/PhabricatorAuthRegisterController.php
@@ -1,743 +1,752 @@
<?php
final class PhabricatorAuthRegisterController
extends PhabricatorAuthController {
public function shouldRequireLogin() {
return false;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$account_key = $request->getURIData('akey');
- if ($request->getUser()->isLoggedIn()) {
+ if ($viewer->isLoggedIn()) {
return id(new AphrontRedirectResponse())->setURI('/');
}
+ $invite = $this->loadInvite();
+
$is_setup = false;
if (strlen($account_key)) {
$result = $this->loadAccountForRegistrationOrLinking($account_key);
list($account, $provider, $response) = $result;
$is_default = false;
} else if ($this->isFirstTimeSetup()) {
- list($account, $provider, $response) = $this->loadSetupAccount();
+ $account = null;
+ $provider = null;
+ $response = null;
$is_default = true;
$is_setup = true;
} else {
- list($account, $provider, $response) = $this->loadDefaultAccount();
+ list($account, $provider, $response) = $this->loadDefaultAccount($invite);
$is_default = true;
}
if ($response) {
return $response;
}
- $invite = $this->loadInvite();
-
- if (!$provider->shouldAllowRegistration()) {
- if ($invite) {
- // If the user has an invite, we allow them to register with any
- // provider, even a login-only provider.
- } else {
- // TODO: This is a routine error if you click "Login" on an external
- // auth source which doesn't allow registration. The error should be
- // more tailored.
+ if (!$is_setup) {
+ if (!$provider->shouldAllowRegistration()) {
+ if ($invite) {
+ // If the user has an invite, we allow them to register with any
+ // provider, even a login-only provider.
+ } else {
+ // TODO: This is a routine error if you click "Login" on an external
+ // auth source which doesn't allow registration. The error should be
+ // more tailored.
- return $this->renderError(
- pht(
- 'The account you are attempting to register with uses an '.
- 'authentication provider ("%s") which does not allow '.
- 'registration. An administrator may have recently disabled '.
- 'registration with this provider.',
- $provider->getProviderName()));
+ return $this->renderError(
+ pht(
+ 'The account you are attempting to register with uses an '.
+ 'authentication provider ("%s") which does not allow '.
+ 'registration. An administrator may have recently disabled '.
+ 'registration with this provider.',
+ $provider->getProviderName()));
+ }
}
}
$errors = array();
$user = new PhabricatorUser();
- $default_username = $account->getUsername();
- $default_realname = $account->getRealName();
+ if ($is_setup) {
+ $default_username = null;
+ $default_realname = null;
+ $default_email = null;
+ } else {
+ $default_username = $account->getUsername();
+ $default_realname = $account->getRealName();
+ $default_email = $account->getEmail();
+ }
$account_type = PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT;
$content_source = PhabricatorContentSource::newFromRequest($request);
- $default_email = $account->getEmail();
-
if ($invite) {
$default_email = $invite->getEmailAddress();
}
if ($default_email !== null) {
if (!PhabricatorUserEmail::isValidAddress($default_email)) {
$errors[] = pht(
'The email address associated with this external account ("%s") is '.
'not a valid email address and can not be used to register a '.
'Phabricator account. Choose a different, valid address.',
phutil_tag('strong', array(), $default_email));
$default_email = null;
}
}
if ($default_email !== null) {
// We should bypass policy here because e.g. limiting an application use
// to a subset of users should not allow the others to overwrite
// configured application emails.
$application_email = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withAddresses(array($default_email))
->executeOne();
if ($application_email) {
$errors[] = pht(
'The email address associated with this account ("%s") is '.
'already in use by an application and can not be used to '.
'register a new Phabricator account. Choose a different, valid '.
'address.',
phutil_tag('strong', array(), $default_email));
$default_email = null;
}
}
$show_existing = null;
if ($default_email !== null) {
// If the account source provided an email, but it's not allowed by
// the configuration, roadblock the user. Previously, we let the user
// pick a valid email address instead, but this does not align well with
// user expectation and it's not clear the cases it enables are valuable.
// See discussion in T3472.
if (!PhabricatorUserEmail::isAllowedAddress($default_email)) {
$debug_email = new PHUIInvisibleCharacterView($default_email);
return $this->renderError(
array(
pht(
'The account you are attempting to register with has an invalid '.
'email address (%s). This Phabricator install only allows '.
'registration with specific email addresses:',
$debug_email),
phutil_tag('br'),
phutil_tag('br'),
PhabricatorUserEmail::describeAllowedAddresses(),
));
}
// If the account source provided an email, but another account already
// has that email, just pretend we didn't get an email.
if ($default_email !== null) {
$same_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$default_email);
if ($same_email) {
if ($invite) {
// We're allowing this to continue. The fact that we loaded the
// invite means that the address is nonprimary and unverified and
// we're OK to steal it.
} else {
$show_existing = $default_email;
$default_email = null;
}
}
}
}
if ($show_existing !== null) {
if (!$request->getInt('phase')) {
return $this->newDialog()
->setTitle(pht('Email Address Already in Use'))
->addHiddenInput('phase', 1)
->appendParagraph(
pht(
'You are creating a new Phabricator account linked to an '.
'existing external account from outside Phabricator.'))
->appendParagraph(
pht(
'The email address ("%s") associated with the external account '.
'is already in use by an existing Phabricator account. Multiple '.
'Phabricator accounts may not have the same email address, so '.
'you can not use this email address to register a new '.
'Phabricator account.',
phutil_tag('strong', array(), $show_existing)))
->appendParagraph(
pht(
'If you want to register a new account, continue with this '.
'registration workflow and choose a new, unique email address '.
'for the new account.'))
->appendParagraph(
pht(
'If you want to link an existing Phabricator account to this '.
'external account, do not continue. Instead: log in to your '.
'existing account, then go to "Settings" and link the account '.
'in the "External Accounts" panel.'))
->appendParagraph(
pht(
'If you continue, you will create a new account. You will not '.
'be able to link this external account to an existing account.'))
->addCancelButton('/auth/login/', pht('Cancel'))
->addSubmitButton(pht('Create New Account'));
} else {
$errors[] = pht(
'The external account you are registering with has an email address '.
'that is already in use ("%s") by an existing Phabricator account. '.
'Choose a new, valid email address to register a new Phabricator '.
'account.',
phutil_tag('strong', array(), $show_existing));
}
}
$profile = id(new PhabricatorRegistrationProfile())
->setDefaultUsername($default_username)
->setDefaultEmail($default_email)
->setDefaultRealName($default_realname)
->setCanEditUsername(true)
->setCanEditEmail(($default_email === null))
->setCanEditRealName(true)
->setShouldVerifyEmail(false);
$event_type = PhabricatorEventType::TYPE_AUTH_WILLREGISTERUSER;
$event_data = array(
'account' => $account,
'profile' => $profile,
);
$event = id(new PhabricatorEvent($event_type, $event_data))
->setUser($user);
PhutilEventEngine::dispatchEvent($event);
$default_username = $profile->getDefaultUsername();
$default_email = $profile->getDefaultEmail();
$default_realname = $profile->getDefaultRealName();
$can_edit_username = $profile->getCanEditUsername();
$can_edit_email = $profile->getCanEditEmail();
$can_edit_realname = $profile->getCanEditRealName();
- $must_set_password = $provider->shouldRequireRegistrationPassword();
+ if ($is_setup) {
+ $must_set_password = false;
+ } else {
+ $must_set_password = $provider->shouldRequireRegistrationPassword();
+ }
$can_edit_anything = $profile->getCanEditAnything() || $must_set_password;
$force_verify = $profile->getShouldVerifyEmail();
// Automatically verify the administrator's email address during first-time
// setup.
if ($is_setup) {
$force_verify = true;
}
$value_username = $default_username;
$value_realname = $default_realname;
$value_email = $default_email;
$value_password = null;
$require_real_name = PhabricatorEnv::getEnvConfig('user.require-real-name');
$e_username = strlen($value_username) ? null : true;
$e_realname = $require_real_name ? true : null;
$e_email = strlen($value_email) ? null : true;
$e_password = true;
$e_captcha = true;
$skip_captcha = false;
if ($invite) {
// If the user is accepting an invite, assume they're trustworthy enough
// that we don't need to CAPTCHA them.
$skip_captcha = true;
}
$min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length');
$min_len = (int)$min_len;
$from_invite = $request->getStr('invite');
if ($from_invite && $can_edit_username) {
$value_username = $request->getStr('username');
$e_username = null;
}
$try_register =
($request->isFormPost() || !$can_edit_anything) &&
!$from_invite &&
($request->getInt('phase') != 1);
if ($try_register) {
$errors = array();
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
if ($must_set_password && !$skip_captcha) {
$e_captcha = pht('Again');
$captcha_ok = AphrontFormRecaptchaControl::processCaptcha($request);
if (!$captcha_ok) {
$errors[] = pht('Captcha response is incorrect, try again.');
$e_captcha = pht('Invalid');
}
}
if ($can_edit_username) {
$value_username = $request->getStr('username');
if (!strlen($value_username)) {
$e_username = pht('Required');
$errors[] = pht('Username is required.');
} else if (!PhabricatorUser::validateUsername($value_username)) {
$e_username = pht('Invalid');
$errors[] = PhabricatorUser::describeValidUsername();
} else {
$e_username = null;
}
}
if ($must_set_password) {
$value_password = $request->getStr('password');
$value_confirm = $request->getStr('confirm');
$password_envelope = new PhutilOpaqueEnvelope($value_password);
$confirm_envelope = new PhutilOpaqueEnvelope($value_confirm);
$engine = id(new PhabricatorAuthPasswordEngine())
->setViewer($user)
->setContentSource($content_source)
->setPasswordType($account_type)
->setObject($user);
try {
$engine->checkNewPassword($password_envelope, $confirm_envelope);
$e_password = null;
} catch (PhabricatorAuthPasswordException $ex) {
$errors[] = $ex->getMessage();
$e_password = $ex->getPasswordError();
}
}
if ($can_edit_email) {
$value_email = $request->getStr('email');
if (!strlen($value_email)) {
$e_email = pht('Required');
$errors[] = pht('Email is required.');
} else if (!PhabricatorUserEmail::isValidAddress($value_email)) {
$e_email = pht('Invalid');
$errors[] = PhabricatorUserEmail::describeValidAddresses();
} else if (!PhabricatorUserEmail::isAllowedAddress($value_email)) {
$e_email = pht('Disallowed');
$errors[] = PhabricatorUserEmail::describeAllowedAddresses();
} else {
$e_email = null;
}
}
if ($can_edit_realname) {
$value_realname = $request->getStr('realName');
if (!strlen($value_realname) && $require_real_name) {
$e_realname = pht('Required');
$errors[] = pht('Real name is required.');
} else {
$e_realname = null;
}
}
if (!$errors) {
- $image = $this->loadProfilePicture($account);
- if ($image) {
- $user->setProfileImagePHID($image->getPHID());
+ if (!$is_setup) {
+ $image = $this->loadProfilePicture($account);
+ if ($image) {
+ $user->setProfileImagePHID($image->getPHID());
+ }
}
try {
$verify_email = false;
if ($force_verify) {
$verify_email = true;
}
- if ($value_email === $default_email) {
- if ($account->getEmailVerified()) {
- $verify_email = true;
- }
+ if (!$is_setup) {
+ if ($value_email === $default_email) {
+ if ($account->getEmailVerified()) {
+ $verify_email = true;
+ }
- if ($provider->shouldTrustEmails()) {
- $verify_email = true;
- }
+ if ($provider->shouldTrustEmails()) {
+ $verify_email = true;
+ }
- if ($invite) {
- $verify_email = true;
+ if ($invite) {
+ $verify_email = true;
+ }
}
}
$email_obj = null;
if ($invite) {
// If we have a valid invite, this email may exist but be
// nonprimary and unverified, so we'll reassign it.
$email_obj = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$value_email);
}
if (!$email_obj) {
$email_obj = id(new PhabricatorUserEmail())
->setAddress($value_email);
}
$email_obj->setIsVerified((int)$verify_email);
$user->setUsername($value_username);
$user->setRealname($value_realname);
if ($is_setup) {
$must_approve = false;
} else if ($invite) {
$must_approve = false;
} else {
$must_approve = PhabricatorEnv::getEnvConfig(
'auth.require-approval');
}
if ($must_approve) {
$user->setIsApproved(0);
} else {
$user->setIsApproved(1);
}
if ($invite) {
$allow_reassign_email = true;
} else {
$allow_reassign_email = false;
}
$user->openTransaction();
$editor = id(new PhabricatorUserEditor())
->setActor($user);
$editor->createNewUser($user, $email_obj, $allow_reassign_email);
if ($must_set_password) {
$password_object = PhabricatorAuthPassword::initializeNewPassword(
$user,
$account_type);
$password_object
->setPassword($password_envelope, $user)
->save();
}
if ($is_setup) {
$xactions = array();
$xactions[] = id(new PhabricatorUserTransaction())
->setTransactionType(
PhabricatorUserEmpowerTransaction::TRANSACTIONTYPE)
->setNewValue(true);
$actor = PhabricatorUser::getOmnipotentUser();
$content_source = PhabricatorContentSource::newFromRequest(
$request);
$people_application_phid = id(new PhabricatorPeopleApplication())
->getPHID();
$transaction_editor = id(new PhabricatorUserTransactionEditor())
->setActor($actor)
->setActingAsPHID($people_application_phid)
->setContentSource($content_source)
->setContinueOnMissingFields(true);
$transaction_editor->applyTransactions($user, $xactions);
}
- $account->setUserPHID($user->getPHID());
- $provider->willRegisterAccount($account);
- $account->save();
+ if (!$is_setup) {
+ $account->setUserPHID($user->getPHID());
+ $provider->willRegisterAccount($account);
+ $account->save();
+ }
$user->saveTransaction();
if (!$email_obj->getIsVerified()) {
$email_obj->sendVerificationEmail($user);
}
if ($must_approve) {
$this->sendWaitingForApprovalEmail($user);
}
if ($invite) {
$invite->setAcceptedByPHID($user->getPHID())->save();
}
return $this->loginUser($user);
} catch (AphrontDuplicateKeyQueryException $exception) {
$same_username = id(new PhabricatorUser())->loadOneWhere(
'userName = %s',
$user->getUserName());
$same_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$value_email);
if ($same_username) {
$e_username = pht('Duplicate');
$errors[] = pht('Another user already has that username.');
}
if ($same_email) {
// TODO: See T3340.
$e_email = pht('Duplicate');
$errors[] = pht('Another user already has that email.');
}
if (!$same_username && !$same_email) {
throw $exception;
}
}
}
unset($unguarded);
}
$form = id(new AphrontFormView())
->setUser($request->getUser())
->addHiddenInput('phase', 2);
if (!$is_default) {
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('External Account'))
->setValue(
id(new PhabricatorAuthAccountView())
->setUser($request->getUser())
->setExternalAccount($account)
->setAuthProvider($provider)));
}
-
if ($can_edit_username) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Username'))
->setName('username')
->setValue($value_username)
->setError($e_username));
} else {
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Username'))
->setValue($value_username)
->setError($e_username));
}
if ($can_edit_realname) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Real Name'))
->setName('realName')
->setValue($value_realname)
->setError($e_realname));
}
if ($must_set_password) {
$form->appendChild(
id(new AphrontFormPasswordControl())
->setLabel(pht('Password'))
->setName('password')
->setError($e_password));
$form->appendChild(
id(new AphrontFormPasswordControl())
->setLabel(pht('Confirm Password'))
->setName('confirm')
->setError($e_password)
->setCaption(
$min_len
? pht('Minimum length of %d characters.', $min_len)
: null));
}
if ($can_edit_email) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Email'))
->setName('email')
->setValue($value_email)
->setCaption(PhabricatorUserEmail::describeAllowedAddresses())
->setError($e_email));
}
if ($must_set_password && !$skip_captcha) {
$form->appendChild(
id(new AphrontFormRecaptchaControl())
->setLabel(pht('Captcha'))
->setError($e_captcha));
}
$submit = id(new AphrontFormSubmitControl());
if ($is_setup) {
$submit
->setValue(pht('Create Admin Account'));
} else {
$submit
->addCancelButton($this->getApplicationURI('start/'))
->setValue(pht('Register Account'));
}
$form->appendChild($submit);
$crumbs = $this->buildApplicationCrumbs();
if ($is_setup) {
$crumbs->addTextCrumb(pht('Setup Admin Account'));
$title = pht('Welcome to Phabricator');
} else {
$crumbs->addTextCrumb(pht('Register'));
$crumbs->addTextCrumb($provider->getProviderName());
$title = pht('Create a New Account');
}
$crumbs->setBorder(true);
$welcome_view = null;
if ($is_setup) {
$welcome_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setTitle(pht('Welcome to Phabricator'))
->appendChild(
pht(
'Installation is complete. Register your administrator account '.
'below to log in. You will be able to configure options and add '.
- 'other authentication mechanisms (like LDAP or OAuth) later on.'));
+ 'authentication mechanisms later on.'));
}
$object_box = id(new PHUIObjectBoxView())
->setForm($form)
->setFormErrors($errors);
$invite_header = null;
if ($invite) {
$invite_header = $this->renderInviteHeader($invite);
}
$header = id(new PHUIHeaderView())
->setHeader($title);
$view = id(new PHUITwoColumnView())
->setHeader($header)
- ->setFooter(array(
- $welcome_view,
- $invite_header,
- $object_box,
- ));
+ ->setFooter(
+ array(
+ $welcome_view,
+ $invite_header,
+ $object_box,
+ ));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
- private function loadDefaultAccount() {
+ private function loadDefaultAccount($invite) {
$providers = PhabricatorAuthProvider::getAllEnabledProviders();
$account = null;
$provider = null;
$response = null;
foreach ($providers as $key => $candidate_provider) {
- if (!$candidate_provider->shouldAllowRegistration()) {
- unset($providers[$key]);
- continue;
+ if (!$invite) {
+ if (!$candidate_provider->shouldAllowRegistration()) {
+ unset($providers[$key]);
+ continue;
+ }
}
+
if (!$candidate_provider->isDefaultRegistrationProvider()) {
unset($providers[$key]);
}
}
if (!$providers) {
$response = $this->renderError(
pht(
'There are no configured default registration providers.'));
return array($account, $provider, $response);
} else if (count($providers) > 1) {
$response = $this->renderError(
pht('There are too many configured default registration providers.'));
return array($account, $provider, $response);
}
$provider = head($providers);
- $account = $provider->getDefaultExternalAccount();
+ $account = $provider->newDefaultExternalAccount();
return array($account, $provider, $response);
}
- private function loadSetupAccount() {
- $provider = new PhabricatorPasswordAuthProvider();
- $provider->attachProviderConfig(
- id(new PhabricatorAuthProviderConfig())
- ->setShouldAllowRegistration(1)
- ->setShouldAllowLogin(1)
- ->setIsEnabled(true));
-
- $account = $provider->getDefaultExternalAccount();
- $response = null;
- return array($account, $provider, $response);
- }
-
private function loadProfilePicture(PhabricatorExternalAccount $account) {
$phid = $account->getProfileImagePHID();
if (!$phid) {
return null;
}
// NOTE: Use of omnipotent user is okay here because the registering user
// can not control the field value, and we can't use their user object to
// do meaningful policy checks anyway since they have not registered yet.
// Reaching this means the user holds the account secret key and the
// registration secret key, and thus has permission to view the image.
$file = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($phid))
->executeOne();
if (!$file) {
return null;
}
$xform = PhabricatorFileTransform::getTransformByKey(
PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE);
return $xform->executeTransform($file);
}
protected function renderError($message) {
return $this->renderErrorPage(
pht('Registration Failed'),
array($message));
}
private function sendWaitingForApprovalEmail(PhabricatorUser $user) {
$title = '[Phabricator] '.pht(
'New User "%s" Awaiting Approval',
$user->getUsername());
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection(
pht(
'Newly registered user "%s" is awaiting account approval by an '.
'administrator.',
$user->getUsername()));
$body->addLinkSection(
pht('APPROVAL QUEUE'),
PhabricatorEnv::getProductionURI(
'/people/query/approval/'));
$body->addLinkSection(
pht('DISABLE APPROVAL QUEUE'),
PhabricatorEnv::getProductionURI(
'/config/edit/auth.require-approval/'));
$admins = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIsAdmin(true)
->execute();
if (!$admins) {
return;
}
$mail = id(new PhabricatorMetaMTAMail())
->addTos(mpull($admins, 'getPHID'))
->setSubject($title)
->setBody($body->render())
->saveAndSend();
}
}
diff --git a/src/applications/auth/controller/PhabricatorAuthSetExternalController.php b/src/applications/auth/controller/PhabricatorAuthSetExternalController.php
new file mode 100644
index 000000000..51dfcab53
--- /dev/null
+++ b/src/applications/auth/controller/PhabricatorAuthSetExternalController.php
@@ -0,0 +1,110 @@
+<?php
+
+final class PhabricatorAuthSetExternalController
+ extends PhabricatorAuthController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+
+ $configs = id(new PhabricatorAuthProviderConfigQuery())
+ ->setViewer($viewer)
+ ->withIsEnabled(true)
+ ->execute();
+
+ $linkable = array();
+ foreach ($configs as $config) {
+ if (!$config->getShouldAllowLink()) {
+ continue;
+ }
+
+ // For now, only buttons get to appear here: for example, we can't
+ // reasonably embed an entire LDAP form into this UI.
+ $provider = $config->getProvider();
+ if (!$provider->isLoginFormAButton()) {
+ continue;
+ }
+
+ $linkable[] = $config;
+ }
+
+ if (!$linkable) {
+ return $this->newDialog()
+ ->setTitle(pht('No Linkable External Providers'))
+ ->appendParagraph(
+ pht(
+ 'Currently, there are no configured external auth providers '.
+ 'which you can link your account to.'))
+ ->addCancelButton('/');
+ }
+
+ $text = PhabricatorAuthMessage::loadMessageText(
+ $viewer,
+ PhabricatorAuthLinkMessageType::MESSAGEKEY);
+ if (!strlen($text)) {
+ $text = pht(
+ 'You can link your Phabricator account to an external account to '.
+ 'allow you to log in more easily in the future. To continue, choose '.
+ 'an account to link below. If you prefer not to link your account, '.
+ 'you can skip this step.');
+ }
+
+ $remarkup_view = new PHUIRemarkupView($viewer, $text);
+ $remarkup_view = phutil_tag(
+ 'div',
+ array(
+ 'class' => 'phui-object-box-instructions',
+ ),
+ $remarkup_view);
+
+ PhabricatorCookies::setClientIDCookie($request);
+
+ $view = array();
+ foreach ($configs as $config) {
+ $provider = $config->getProvider();
+
+ $form = $provider->buildLinkForm($this);
+
+ if ($provider->isLoginFormAButton()) {
+ require_celerity_resource('auth-css');
+ $form = phutil_tag(
+ 'div',
+ array(
+ 'class' => 'phabricator-link-button pl',
+ ),
+ $form);
+ }
+
+ $view[] = $form;
+ }
+
+ $form = id(new AphrontFormView())
+ ->setViewer($viewer)
+ ->appendControl(
+ id(new AphrontFormSubmitControl())
+ ->addCancelButton('/', pht('Skip This Step')));
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader(pht('Link External Account'));
+
+ $box = id(new PHUIObjectBoxView())
+ ->setViewer($viewer)
+ ->setHeader($header)
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->appendChild($remarkup_view)
+ ->appendChild($view)
+ ->appendChild($form);
+
+ $main_view = id(new PHUITwoColumnView())
+ ->setFooter($box);
+
+ $crumbs = $this->buildApplicationCrumbs()
+ ->addTextCrumb(pht('Link External Account'))
+ ->setBorder(true);
+
+ return $this->newPage()
+ ->setTitle(pht('Link External Account'))
+ ->setCrumbs($crumbs)
+ ->appendChild($main_view);
+ }
+
+}
diff --git a/src/applications/auth/controller/PhabricatorAuthStartController.php b/src/applications/auth/controller/PhabricatorAuthStartController.php
index 29fa7e0b9..72cbbea5a 100644
--- a/src/applications/auth/controller/PhabricatorAuthStartController.php
+++ b/src/applications/auth/controller/PhabricatorAuthStartController.php
@@ -1,332 +1,361 @@
<?php
final class PhabricatorAuthStartController
extends PhabricatorAuthController {
public function shouldRequireLogin() {
return false;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getUser();
if ($viewer->isLoggedIn()) {
// Kick the user home if they are already logged in.
return id(new AphrontRedirectResponse())->setURI('/');
}
if ($request->isAjax()) {
return $this->processAjaxRequest();
}
if ($request->isConduit()) {
return $this->processConduitRequest();
}
// If the user gets this far, they aren't logged in, so if they have a
// user session token we can conclude that it's invalid: if it was valid,
// they'd have been logged in above and never made it here. Try to clear
// it and warn the user they may need to nuke their cookies.
$session_token = $request->getCookie(PhabricatorCookies::COOKIE_SESSION);
$did_clear = $request->getStr('cleared');
if (strlen($session_token)) {
$kind = PhabricatorAuthSessionEngine::getSessionKindFromToken(
$session_token);
switch ($kind) {
case PhabricatorAuthSessionEngine::KIND_ANONYMOUS:
// If this is an anonymous session. It's expected that they won't
// be logged in, so we can just continue.
break;
default:
// The session cookie is invalid, so try to clear it.
$request->clearCookie(PhabricatorCookies::COOKIE_USERNAME);
$request->clearCookie(PhabricatorCookies::COOKIE_SESSION);
// We've previously tried to clear the cookie but we ended up back
// here, so it didn't work. Hard fatal instead of trying again.
if ($did_clear) {
return $this->renderError(
pht(
'Your login session is invalid, and clearing the session '.
'cookie was unsuccessful. Try clearing your browser cookies.'));
}
$redirect_uri = $request->getRequestURI();
- $redirect_uri->setQueryParam('cleared', 1);
+ $redirect_uri->replaceQueryParam('cleared', 1);
return id(new AphrontRedirectResponse())->setURI($redirect_uri);
}
}
// If we just cleared the session cookie and it worked, clean up after
// ourselves by redirecting to get rid of the "cleared" parameter. The
// the workflow will continue normally.
if ($did_clear) {
$redirect_uri = $request->getRequestURI();
- $redirect_uri->setQueryParam('cleared', null);
+ $redirect_uri->removeQueryParam('cleared');
return id(new AphrontRedirectResponse())->setURI($redirect_uri);
}
$providers = PhabricatorAuthProvider::getAllEnabledProviders();
foreach ($providers as $key => $provider) {
if (!$provider->shouldAllowLogin()) {
unset($providers[$key]);
}
}
+ $configs = array();
+ foreach ($providers as $provider) {
+ $configs[] = $provider->getProviderConfig();
+ }
+
if (!$providers) {
if ($this->isFirstTimeSetup()) {
// If this is a fresh install, let the user register their admin
// account.
return id(new AphrontRedirectResponse())
->setURI($this->getApplicationURI('/register/'));
}
return $this->renderError(
pht(
'This Phabricator install is not configured with any enabled '.
'authentication providers which can be used to log in. If you '.
'have accidentally locked yourself out by disabling all providers, '.
'you can use `%s` to recover access to an account.',
'phabricator/bin/auth recover <username>'));
}
$next_uri = $request->getStr('next');
if (!strlen($next_uri)) {
if ($this->getDelegatingController()) {
// Only set a next URI from the request path if this controller was
// delegated to, which happens when a user tries to view a page which
// requires them to login.
// If this controller handled the request directly, we're on the main
// login page, and never want to redirect the user back here after they
// login.
$next_uri = (string)$this->getRequest()->getRequestURI();
}
}
if (!$request->isFormPost()) {
if (strlen($next_uri)) {
PhabricatorCookies::setNextURICookie($request, $next_uri);
}
PhabricatorCookies::setClientIDCookie($request);
}
$auto_response = $this->tryAutoLogin($providers);
if ($auto_response) {
return $auto_response;
}
$invite = $this->loadInvite();
$not_buttons = array();
$are_buttons = array();
$providers = msort($providers, 'getLoginOrder');
foreach ($providers as $provider) {
if ($invite) {
$form = $provider->buildInviteForm($this);
} else {
$form = $provider->buildLoginForm($this);
}
if ($provider->isLoginFormAButton()) {
$are_buttons[] = $form;
} else {
$not_buttons[] = $form;
}
}
$out = array();
$out[] = $not_buttons;
if ($are_buttons) {
require_celerity_resource('auth-css');
foreach ($are_buttons as $key => $button) {
$are_buttons[$key] = phutil_tag(
'div',
array(
'class' => 'phabricator-login-button mmb',
),
$button);
}
// If we only have one button, add a second pretend button so that we
// always have two columns. This makes it easier to get the alignments
// looking reasonable.
if (count($are_buttons) == 1) {
$are_buttons[] = null;
}
$button_columns = id(new AphrontMultiColumnView())
->setFluidLayout(true);
$are_buttons = array_chunk($are_buttons, ceil(count($are_buttons) / 2));
foreach ($are_buttons as $column) {
$button_columns->addColumn($column);
}
$out[] = phutil_tag(
'div',
array(
'class' => 'phabricator-login-buttons',
),
$button_columns);
}
- $handlers = PhabricatorAuthLoginHandler::getAllHandlers();
-
- $delegating_controller = $this->getDelegatingController();
-
- $header = array();
- foreach ($handlers as $handler) {
- $handler = clone $handler;
-
- $handler->setRequest($request);
-
- if ($delegating_controller) {
- $handler->setDelegatingController($delegating_controller);
- }
-
- $header[] = $handler->getAuthLoginHeaderContent();
- }
-
$invite_message = null;
if ($invite) {
$invite_message = $this->renderInviteHeader($invite);
}
$custom_message = $this->newCustomStartMessage();
+ $email_login = $this->newEmailLoginView($configs);
+
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Login'));
$crumbs->setBorder(true);
$title = pht('Login');
$view = array(
- $header,
$invite_message,
$custom_message,
$out,
+ $email_login,
);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
private function processAjaxRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
// We end up here if the user clicks a workflow link that they need to
// login to use. We give them a dialog saying "You need to login...".
if ($request->isDialogFormPost()) {
return id(new AphrontRedirectResponse())->setURI(
$request->getRequestURI());
}
// Often, users end up here by clicking a disabled action link in the UI
// (for example, they might click "Edit Subtasks" on a Maniphest task
// page). After they log in we want to send them back to that main object
// page if we can, since it's confusing to end up on a standalone page with
// only a dialog (particularly if that dialog is another error,
// like a policy exception).
$via_header = AphrontRequest::getViaHeaderName();
$via_uri = AphrontRequest::getHTTPHeader($via_header);
if (strlen($via_uri)) {
PhabricatorCookies::setNextURICookie($request, $via_uri, $force = true);
}
return $this->newDialog()
->setTitle(pht('Login Required'))
->appendParagraph(pht('You must log in to take this action.'))
->addSubmitButton(pht('Log In'))
->addCancelButton('/');
}
private function processConduitRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
// A common source of errors in Conduit client configuration is getting
// the request path wrong. The client will end up here, so make some
// effort to give them a comprehensible error message.
$request_path = $this->getRequest()->getPath();
$conduit_path = '/api/<method>';
$example_path = '/api/conduit.ping';
$message = pht(
'ERROR: You are making a Conduit API request to "%s", but the correct '.
'HTTP request path to use in order to access a COnduit method is "%s" '.
'(for example, "%s"). Check your configuration.',
$request_path,
$conduit_path,
$example_path);
return id(new AphrontPlainTextResponse())->setContent($message);
}
protected function renderError($message) {
return $this->renderErrorPage(
pht('Authentication Failure'),
array($message));
}
private function tryAutoLogin(array $providers) {
$request = $this->getRequest();
// If the user just logged out, don't immediately log them in again.
if ($request->getURIData('loggedout')) {
return null;
}
// If we have more than one provider, we can't autologin because we
// don't know which one the user wants.
if (count($providers) != 1) {
return null;
}
$provider = head($providers);
if (!$provider->supportsAutoLogin()) {
return null;
}
$config = $provider->getProviderConfig();
if (!$config->getShouldAutoLogin()) {
return null;
}
$auto_uri = $provider->getAutoLoginURI($request);
return id(new AphrontRedirectResponse())
->setIsExternal(true)
->setURI($auto_uri);
}
private function newCustomStartMessage() {
$viewer = $this->getViewer();
$text = PhabricatorAuthMessage::loadMessageText(
$viewer,
PhabricatorAuthLoginMessageType::MESSAGEKEY);
if (!strlen($text)) {
return null;
}
$remarkup_view = new PHUIRemarkupView($viewer, $text);
return phutil_tag(
'div',
array(
'class' => 'auth-custom-message',
),
$remarkup_view);
}
+ private function newEmailLoginView(array $configs) {
+ assert_instances_of($configs, 'PhabricatorAuthProviderConfig');
+
+ // Check if password auth is enabled. If it is, the password login form
+ // renders a "Forgot password?" link, so we don't need to provide a
+ // supplemental link.
+
+ $has_password = false;
+ foreach ($configs as $config) {
+ $provider = $config->getProvider();
+ if ($provider instanceof PhabricatorPasswordAuthProvider) {
+ $has_password = true;
+ }
+ }
+
+ if ($has_password) {
+ return null;
+ }
+
+ $view = array(
+ pht('Trouble logging in?'),
+ ' ',
+ phutil_tag(
+ 'a',
+ array(
+ 'href' => '/login/email/',
+ ),
+ pht('Send a login link to your email address.')),
+ );
+
+ return phutil_tag(
+ 'div',
+ array(
+ 'class' => 'auth-custom-message',
+ ),
+ $view);
+ }
+
+
}
diff --git a/src/applications/auth/controller/PhabricatorAuthUnlinkController.php b/src/applications/auth/controller/PhabricatorAuthUnlinkController.php
index e6e1493e5..43e7b1b36 100644
--- a/src/applications/auth/controller/PhabricatorAuthUnlinkController.php
+++ b/src/applications/auth/controller/PhabricatorAuthUnlinkController.php
@@ -1,146 +1,141 @@
<?php
final class PhabricatorAuthUnlinkController
extends PhabricatorAuthController {
- private $providerKey;
-
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
- $this->providerKey = $request->getURIData('pkey');
-
- list($type, $domain) = explode(':', $this->providerKey, 2);
-
- // Check that this account link actually exists. We don't require the
- // provider to exist because we want users to be able to delete links to
- // dead accounts if they want.
- $account = id(new PhabricatorExternalAccount())->loadOneWhere(
- 'accountType = %s AND accountDomain = %s AND userPHID = %s',
- $type,
- $domain,
- $viewer->getPHID());
+ $id = $request->getURIData('id');
+
+ $account = id(new PhabricatorExternalAccountQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($id))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
if (!$account) {
- return $this->renderNoAccountErrorDialog();
+ return new Aphront404Response();
}
- // Check that the provider (if it exists) allows accounts to be unlinked.
- $provider_key = $this->providerKey;
- $provider = PhabricatorAuthProvider::getEnabledProviderByKey($provider_key);
- if ($provider) {
- if (!$provider->shouldAllowAccountUnlink()) {
- return $this->renderNotUnlinkableErrorDialog($provider);
- }
+ $done_uri = '/settings/panel/external/';
+
+ $config = $account->getProviderConfig();
+ $provider = $config->getProvider();
+ if (!$provider->shouldAllowAccountUnlink()) {
+ return $this->renderNotUnlinkableErrorDialog($provider, $done_uri);
}
- // Check that this account isn't the last account which can be used to
- // login. We prevent you from removing the last account.
+ $confirmations = $request->getStrList('confirmations');
+ $confirmations = array_fuse($confirmations);
+
+ if (!$request->isFormOrHisecPost() || !isset($confirmations['unlink'])) {
+ return $this->renderConfirmDialog($confirmations, $config, $done_uri);
+ }
+
+ // Check that this account isn't the only account which can be used to
+ // login. We warn you when you remove your only login account.
if ($account->isUsableForLogin()) {
- $other_accounts = id(new PhabricatorExternalAccount())->loadAllWhere(
- 'userPHID = %s',
- $viewer->getPHID());
+ $other_accounts = id(new PhabricatorExternalAccountQuery())
+ ->setViewer($viewer)
+ ->withUserPHIDs(array($viewer->getPHID()))
+ ->execute();
$valid_accounts = 0;
foreach ($other_accounts as $other_account) {
if ($other_account->isUsableForLogin()) {
$valid_accounts++;
}
}
if ($valid_accounts < 2) {
- return $this->renderLastUsableAccountErrorDialog();
+ if (!isset($confirmations['only'])) {
+ return $this->renderOnlyUsableAccountConfirmDialog(
+ $confirmations,
+ $done_uri);
+ }
}
}
- if ($request->isDialogFormPost()) {
- $account->delete();
+ $workflow_key = sprintf(
+ 'account.unlink(%s)',
+ $account->getPHID());
- id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
- $viewer,
- new PhutilOpaqueEnvelope(
- $request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
+ $hisec_token = id(new PhabricatorAuthSessionEngine())
+ ->setWorkflowKey($workflow_key)
+ ->requireHighSecurityToken($viewer, $request, $done_uri);
- return id(new AphrontRedirectResponse())->setURI($this->getDoneURI());
- }
-
- return $this->renderConfirmDialog();
- }
+ $account->delete();
- private function getDoneURI() {
- return '/settings/panel/external/';
- }
-
- private function renderNoAccountErrorDialog() {
- $dialog = id(new AphrontDialogView())
- ->setUser($this->getRequest()->getUser())
- ->setTitle(pht('No Such Account'))
- ->appendChild(
- pht(
- 'You can not unlink this account because it is not linked.'))
- ->addCancelButton($this->getDoneURI());
+ id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
+ $viewer,
+ new PhutilOpaqueEnvelope(
+ $request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
- return id(new AphrontDialogResponse())->setDialog($dialog);
+ return id(new AphrontRedirectResponse())->setURI($done_uri);
}
private function renderNotUnlinkableErrorDialog(
- PhabricatorAuthProvider $provider) {
+ PhabricatorAuthProvider $provider,
+ $done_uri) {
- $dialog = id(new AphrontDialogView())
- ->setUser($this->getRequest()->getUser())
+ return $this->newDialog()
->setTitle(pht('Permanent Account Link'))
->appendChild(
pht(
'You can not unlink this account because the administrator has '.
- 'configured Phabricator to make links to %s accounts permanent.',
+ 'configured Phabricator to make links to "%s" accounts permanent.',
$provider->getProviderName()))
- ->addCancelButton($this->getDoneURI());
-
- return id(new AphrontDialogResponse())->setDialog($dialog);
+ ->addCancelButton($done_uri);
}
- private function renderLastUsableAccountErrorDialog() {
- $dialog = id(new AphrontDialogView())
- ->setUser($this->getRequest()->getUser())
- ->setTitle(pht('Last Valid Account'))
- ->appendChild(
- pht(
- 'You can not unlink this account because you have no other '.
- 'valid login accounts. If you removed it, you would be unable '.
- 'to log in. Add another authentication method before removing '.
- 'this one.'))
- ->addCancelButton($this->getDoneURI());
+ private function renderOnlyUsableAccountConfirmDialog(
+ array $confirmations,
+ $done_uri) {
- return id(new AphrontDialogResponse())->setDialog($dialog);
+ $confirmations[] = 'only';
+
+ return $this->newDialog()
+ ->setTitle(pht('Unlink Your Only Login Account?'))
+ ->addHiddenInput('confirmations', implode(',', $confirmations))
+ ->appendParagraph(
+ pht(
+ 'This is the only external login account linked to your Phabicator '.
+ 'account. If you remove it, you may no longer be able to log in.'))
+ ->appendParagraph(
+ pht(
+ 'If you lose access to your account, you can recover access by '.
+ 'sending yourself an email login link from the login screen.'))
+ ->addCancelButton($done_uri)
+ ->addSubmitButton(pht('Unlink External Account'));
}
- private function renderConfirmDialog() {
- $provider_key = $this->providerKey;
- $provider = PhabricatorAuthProvider::getEnabledProviderByKey($provider_key);
-
- if ($provider) {
- $title = pht('Unlink "%s" Account?', $provider->getProviderName());
- $body = pht(
- 'You will no longer be able to use your %s account to '.
- 'log in to Phabricator.',
- $provider->getProviderName());
- } else {
- $title = pht('Unlink Account?');
- $body = pht(
- 'You will no longer be able to use this account to log in '.
- 'to Phabricator.');
- }
+ private function renderConfirmDialog(
+ array $confirmations,
+ PhabricatorAuthProviderConfig $config,
+ $done_uri) {
- $dialog = id(new AphrontDialogView())
- ->setUser($this->getRequest()->getUser())
+ $confirmations[] = 'unlink';
+ $provider = $config->getProvider();
+
+ $title = pht('Unlink "%s" Account?', $provider->getProviderName());
+ $body = pht(
+ 'You will no longer be able to use your %s account to '.
+ 'log in to Phabricator.',
+ $provider->getProviderName());
+
+ return $this->newDialog()
->setTitle($title)
+ ->addHiddenInput('confirmations', implode(',', $confirmations))
->appendParagraph($body)
->appendParagraph(
pht(
'Note: Unlinking an authentication provider will terminate any '.
'other active login sessions.'))
->addSubmitButton(pht('Unlink Account'))
- ->addCancelButton($this->getDoneURI());
-
- return id(new AphrontDialogResponse())->setDialog($dialog);
+ ->addCancelButton($done_uri);
}
}
diff --git a/src/applications/auth/controller/PhabricatorEmailLoginController.php b/src/applications/auth/controller/PhabricatorEmailLoginController.php
index f57a29b11..76b288f05 100644
--- a/src/applications/auth/controller/PhabricatorEmailLoginController.php
+++ b/src/applications/auth/controller/PhabricatorEmailLoginController.php
@@ -1,166 +1,237 @@
<?php
final class PhabricatorEmailLoginController
extends PhabricatorAuthController {
public function shouldRequireLogin() {
return false;
}
public function handleRequest(AphrontRequest $request) {
-
- if (!PhabricatorPasswordAuthProvider::getPasswordProvider()) {
- return new Aphront400Response();
- }
+ $viewer = $this->getViewer();
+ $is_logged_in = $viewer->isLoggedIn();
$e_email = true;
$e_captcha = true;
$errors = array();
- $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
+ if ($is_logged_in) {
+ if (!$this->isPasswordAuthEnabled()) {
+ return $this->newDialog()
+ ->setTitle(pht('No Password Auth'))
+ ->appendParagraph(
+ pht(
+ 'Password authentication is not enabled and you are already '.
+ 'logged in. There is nothing for you here.'))
+ ->addCancelButton('/', pht('Continue'));
+ }
+
+ $v_email = $viewer->loadPrimaryEmailAddress();
+ } else {
+ $v_email = $request->getStr('email');
+ }
if ($request->isFormPost()) {
$e_email = null;
$e_captcha = pht('Again');
- $captcha_ok = AphrontFormRecaptchaControl::processCaptcha($request);
- if (!$captcha_ok) {
- $errors[] = pht('Captcha response is incorrect, try again.');
- $e_captcha = pht('Invalid');
+ if (!$is_logged_in) {
+ $captcha_ok = AphrontFormRecaptchaControl::processCaptcha($request);
+ if (!$captcha_ok) {
+ $errors[] = pht('Captcha response is incorrect, try again.');
+ $e_captcha = pht('Invalid');
+ }
}
- $email = $request->getStr('email');
- if (!strlen($email)) {
+ if (!strlen($v_email)) {
$errors[] = pht('You must provide an email address.');
$e_email = pht('Required');
}
if (!$errors) {
// NOTE: Don't validate the email unless the captcha is good; this makes
// it expensive to fish for valid email addresses while giving the user
// a better error if they goof their email.
$target_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
- $email);
+ $v_email);
$target_user = null;
if ($target_email) {
$target_user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$target_email->getUserPHID());
}
if (!$target_user) {
$errors[] =
pht('There is no account associated with that email address.');
$e_email = pht('Invalid');
}
// If this address is unverified, only send a reset link to it if
// the account has no verified addresses. This prevents an opportunistic
// attacker from compromising an account if a user adds an email
// address but mistypes it and doesn't notice.
// (For a newly created account, all the addresses may be unverified,
// which is why we'll send to an unverified address in that case.)
if ($target_email && !$target_email->getIsVerified()) {
$verified_addresses = id(new PhabricatorUserEmail())->loadAllWhere(
'userPHID = %s AND isVerified = 1',
$target_email->getUserPHID());
if ($verified_addresses) {
$errors[] = pht(
'That email address is not verified, but the account it is '.
'connected to has at least one other verified address. When an '.
'account has at least one verified address, you can only send '.
'password reset links to one of the verified addresses. Try '.
'a verified address instead.');
$e_email = pht('Unverified');
}
}
if (!$errors) {
- $engine = new PhabricatorAuthSessionEngine();
- $uri = $engine->getOneTimeLoginURI(
+ $body = $this->newAccountLoginMailBody(
$target_user,
- null,
- PhabricatorAuthSessionEngine::ONETIME_RESET);
-
- if ($is_serious) {
- $body = pht(
- "You can use this link to reset your Phabricator password:".
- "\n\n %s\n",
- $uri);
- } else {
- $body = pht(
- "Condolences on forgetting your password. You can use this ".
- "link to reset it:\n\n".
- " %s\n\n".
- "After you set a new password, consider writing it down on a ".
- "sticky note and attaching it to your monitor so you don't ".
- "forget again! Choosing a very short, easy-to-remember password ".
- "like \"cat\" or \"1234\" might also help.\n\n".
- "Best Wishes,\nPhabricator\n",
- $uri);
+ $is_logged_in);
+ if ($is_logged_in) {
+ $subject = pht('[Phabricator] Account Password Link');
+ $instructions = pht(
+ 'An email has been sent containing a link you can use to set '.
+ 'a password for your account.');
+ } else {
+ $subject = pht('[Phabricator] Account Login Link');
+ $instructions = pht(
+ 'An email has been sent containing a link you can use to log '.
+ 'in to your account.');
}
$mail = id(new PhabricatorMetaMTAMail())
- ->setSubject(pht('[Phabricator] Password Reset'))
+ ->setSubject($subject)
->setForceDelivery(true)
->addRawTos(array($target_email->getAddress()))
->setBody($body)
->saveAndSend();
return $this->newDialog()
->setTitle(pht('Check Your Email'))
->setShortTitle(pht('Email Sent'))
- ->appendParagraph(
- pht('An email has been sent with a link you can use to log in.'))
+ ->appendParagraph($instructions)
->addCancelButton('/', pht('Done'));
}
}
}
- $error_view = null;
- if ($errors) {
- $error_view = new PHUIInfoView();
- $error_view->setErrors($errors);
+ $form = id(new AphrontFormView())
+ ->setViewer($viewer);
+
+ if ($this->isPasswordAuthEnabled()) {
+ if ($is_logged_in) {
+ $title = pht('Set Password');
+ $form->appendRemarkupInstructions(
+ pht(
+ 'A password reset link will be sent to your primary email '.
+ 'address. Follow the link to set an account password.'));
+ } else {
+ $title = pht('Password Reset');
+ $form->appendRemarkupInstructions(
+ pht(
+ 'To reset your password, provide your email address. An email '.
+ 'with a login link will be sent to you.'));
+ }
+ } else {
+ $title = pht('Email Login');
+ $form->appendRemarkupInstructions(
+ pht(
+ 'To access your account, provide your email address. An email '.
+ 'with a login link will be sent to you.'));
+ }
+
+ if ($is_logged_in) {
+ $address_control = new AphrontFormStaticControl();
+ } else {
+ $address_control = id(new AphrontFormTextControl())
+ ->setName('email')
+ ->setError($e_email);
}
- $email_auth = new PHUIFormLayoutView();
- $email_auth->appendChild($error_view);
- $email_auth
- ->setUser($request->getUser())
- ->setFullWidth(true)
- ->appendChild(
- id(new AphrontFormTextControl())
- ->setLabel(pht('Email'))
- ->setName('email')
- ->setValue($request->getStr('email'))
- ->setError($e_email))
- ->appendChild(
+ $address_control
+ ->setLabel(pht('Email Address'))
+ ->setValue($v_email);
+
+ $form
+ ->appendControl($address_control);
+
+ if (!$is_logged_in) {
+ $form->appendControl(
id(new AphrontFormRecaptchaControl())
->setLabel(pht('Captcha'))
->setError($e_captcha));
+ }
- $crumbs = $this->buildApplicationCrumbs();
- $crumbs->addTextCrumb(pht('Reset Password'));
- $crumbs->setBorder(true);
+ return $this->newDialog()
+ ->setTitle($title)
+ ->setErrors($errors)
+ ->setWidth(AphrontDialogView::WIDTH_FORM)
+ ->appendForm($form)
+ ->addCancelButton('/auth/start/')
+ ->addSubmitButton(pht('Send Email'));
+ }
- $dialog = new AphrontDialogView();
- $dialog->setUser($request->getUser());
- $dialog->setTitle(pht('Forgot Password / Email Login'));
- $dialog->appendChild($email_auth);
- $dialog->addSubmitButton(pht('Send Email'));
- $dialog->setSubmitURI('/login/email/');
+ private function newAccountLoginMailBody(
+ PhabricatorUser $user,
+ $is_logged_in) {
- return $this->newPage()
- ->setTitle(pht('Forgot Password'))
- ->setCrumbs($crumbs)
- ->appendChild($dialog);
+ $engine = new PhabricatorAuthSessionEngine();
+ $uri = $engine->getOneTimeLoginURI(
+ $user,
+ null,
+ PhabricatorAuthSessionEngine::ONETIME_RESET);
+ $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
+ $have_passwords = $this->isPasswordAuthEnabled();
+
+ if ($have_passwords) {
+ if ($is_logged_in) {
+ $body = pht(
+ 'You can use this link to set a password on your account:'.
+ "\n\n %s\n",
+ $uri);
+ } else if ($is_serious) {
+ $body = pht(
+ "You can use this link to reset your Phabricator password:".
+ "\n\n %s\n",
+ $uri);
+ } else {
+ $body = pht(
+ "Condolences on forgetting your password. You can use this ".
+ "link to reset it:\n\n".
+ " %s\n\n".
+ "After you set a new password, consider writing it down on a ".
+ "sticky note and attaching it to your monitor so you don't ".
+ "forget again! Choosing a very short, easy-to-remember password ".
+ "like \"cat\" or \"1234\" might also help.\n\n".
+ "Best Wishes,\nPhabricator\n",
+ $uri);
+
+ }
+ } else {
+ $body = pht(
+ "You can use this login link to regain access to your Phabricator ".
+ "account:".
+ "\n\n".
+ " %s\n",
+ $uri);
+ }
+
+ return $body;
}
+ private function isPasswordAuthEnabled() {
+ return (bool)PhabricatorPasswordAuthProvider::getPasswordProvider();
+ }
}
diff --git a/src/applications/auth/controller/config/PhabricatorAuthDisableController.php b/src/applications/auth/controller/config/PhabricatorAuthDisableController.php
index 5863aceca..252f159ec 100644
--- a/src/applications/auth/controller/config/PhabricatorAuthDisableController.php
+++ b/src/applications/auth/controller/config/PhabricatorAuthDisableController.php
@@ -1,90 +1,89 @@
<?php
final class PhabricatorAuthDisableController
extends PhabricatorAuthProviderConfigController {
public function handleRequest(AphrontRequest $request) {
$this->requireApplicationCapability(
AuthManageProvidersCapability::CAPABILITY);
- $viewer = $request->getUser();
+
+ $viewer = $this->getViewer();
$config_id = $request->getURIData('id');
$action = $request->getURIData('action');
$config = id(new PhabricatorAuthProviderConfigQuery())
->setViewer($viewer)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($config_id))
->executeOne();
if (!$config) {
return new Aphront404Response();
}
$is_enable = ($action === 'enable');
+ $done_uri = $config->getURI();
if ($request->isDialogFormPost()) {
$xactions = array();
$xactions[] = id(new PhabricatorAuthProviderConfigTransaction())
->setTransactionType(
PhabricatorAuthProviderConfigTransaction::TYPE_ENABLE)
->setNewValue((int)$is_enable);
$editor = id(new PhabricatorAuthProviderConfigEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->applyTransactions($config, $xactions);
- return id(new AphrontRedirectResponse())->setURI(
- $this->getApplicationURI());
+ return id(new AphrontRedirectResponse())->setURI($done_uri);
}
if ($is_enable) {
$title = pht('Enable Provider?');
if ($config->getShouldAllowRegistration()) {
$body = pht(
'Do you want to enable this provider? Users will be able to use '.
'their existing external accounts to register new Phabricator '.
'accounts and log in using linked accounts.');
} else {
$body = pht(
'Do you want to enable this provider? Users will be able to log '.
'in to Phabricator using linked accounts.');
}
$button = pht('Enable Provider');
} else {
// TODO: We could tailor this a bit more. In particular, we could
// check if this is the last provider and either prevent if from
// being disabled or force the user through like 35 prompts. We could
// also check if it's the last provider linked to the acting user's
// account and pop a warning like "YOU WILL NO LONGER BE ABLE TO LOGIN
// YOU GOOF, YOU PROBABLY DO NOT MEAN TO DO THIS". None of this is
// critical and we can wait to see how users manage to shoot themselves
- // in the feet. Shortly, `bin/auth` will be able to recover from these
- // types of mistakes.
+ // in the feet.
+
+ // `bin/auth` can recover from these types of mistakes.
$title = pht('Disable Provider?');
$body = pht(
'Do you want to disable this provider? Users will not be able to '.
'register or log in using linked accounts. If there are any users '.
'without other linked authentication mechanisms, they will no longer '.
'be able to log in. If you disable all providers, no one will be '.
'able to log in.');
$button = pht('Disable Provider');
}
- $dialog = id(new AphrontDialogView())
- ->setUser($viewer)
+ return $this->newDialog()
->setTitle($title)
->appendChild($body)
- ->addCancelButton($this->getApplicationURI())
+ ->addCancelButton($done_uri)
->addSubmitButton($button);
-
- return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/auth/controller/config/PhabricatorAuthEditController.php b/src/applications/auth/controller/config/PhabricatorAuthEditController.php
index 6ff0be438..d3cd2fef9 100644
--- a/src/applications/auth/controller/config/PhabricatorAuthEditController.php
+++ b/src/applications/auth/controller/config/PhabricatorAuthEditController.php
@@ -1,382 +1,366 @@
<?php
final class PhabricatorAuthEditController
extends PhabricatorAuthProviderConfigController {
public function handleRequest(AphrontRequest $request) {
$this->requireApplicationCapability(
AuthManageProvidersCapability::CAPABILITY);
- $viewer = $request->getUser();
- $provider_class = $request->getURIData('className');
+
+ $viewer = $this->getViewer();
+ $provider_class = $request->getStr('provider');
$config_id = $request->getURIData('id');
if ($config_id) {
$config = id(new PhabricatorAuthProviderConfigQuery())
->setViewer($viewer)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($config_id))
->executeOne();
if (!$config) {
return new Aphront404Response();
}
$provider = $config->getProvider();
if (!$provider) {
return new Aphront404Response();
}
$is_new = false;
} else {
$provider = null;
$providers = PhabricatorAuthProvider::getAllBaseProviders();
foreach ($providers as $candidate_provider) {
if (get_class($candidate_provider) === $provider_class) {
$provider = $candidate_provider;
break;
}
}
if (!$provider) {
return new Aphront404Response();
}
// TODO: When we have multi-auth providers, support them here.
$configs = id(new PhabricatorAuthProviderConfigQuery())
->setViewer($viewer)
->withProviderClasses(array(get_class($provider)))
->execute();
if ($configs) {
$id = head($configs)->getID();
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setMethod('GET')
->setSubmitURI($this->getApplicationURI('config/edit/'.$id.'/'))
->setTitle(pht('Provider Already Configured'))
->appendChild(
pht(
'This provider ("%s") already exists, and you can not add more '.
'than one instance of it. You can edit the existing provider, '.
'or you can choose a different provider.',
$provider->getProviderName()))
->addCancelButton($this->getApplicationURI('config/new/'))
->addSubmitButton(pht('Edit Existing Provider'));
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$config = $provider->getDefaultProviderConfig();
$provider->attachProviderConfig($config);
$is_new = true;
}
$errors = array();
$v_login = $config->getShouldAllowLogin();
$v_registration = $config->getShouldAllowRegistration();
$v_link = $config->getShouldAllowLink();
$v_unlink = $config->getShouldAllowUnlink();
$v_trust_email = $config->getShouldTrustEmails();
$v_auto_login = $config->getShouldAutoLogin();
if ($request->isFormPost()) {
$properties = $provider->readFormValuesFromRequest($request);
list($errors, $issues, $properties) = $provider->processEditForm(
$request,
$properties);
$xactions = array();
if (!$errors) {
if ($is_new) {
if (!strlen($config->getProviderType())) {
$config->setProviderType($provider->getProviderType());
}
if (!strlen($config->getProviderDomain())) {
$config->setProviderDomain($provider->getProviderDomain());
}
}
$xactions[] = id(new PhabricatorAuthProviderConfigTransaction())
->setTransactionType(
PhabricatorAuthProviderConfigTransaction::TYPE_LOGIN)
->setNewValue($request->getInt('allowLogin', 0));
$xactions[] = id(new PhabricatorAuthProviderConfigTransaction())
->setTransactionType(
PhabricatorAuthProviderConfigTransaction::TYPE_REGISTRATION)
->setNewValue($request->getInt('allowRegistration', 0));
$xactions[] = id(new PhabricatorAuthProviderConfigTransaction())
->setTransactionType(
PhabricatorAuthProviderConfigTransaction::TYPE_LINK)
->setNewValue($request->getInt('allowLink', 0));
$xactions[] = id(new PhabricatorAuthProviderConfigTransaction())
->setTransactionType(
PhabricatorAuthProviderConfigTransaction::TYPE_UNLINK)
->setNewValue($request->getInt('allowUnlink', 0));
$xactions[] = id(new PhabricatorAuthProviderConfigTransaction())
->setTransactionType(
PhabricatorAuthProviderConfigTransaction::TYPE_TRUST_EMAILS)
->setNewValue($request->getInt('trustEmails', 0));
if ($provider->supportsAutoLogin()) {
$xactions[] = id(new PhabricatorAuthProviderConfigTransaction())
->setTransactionType(
PhabricatorAuthProviderConfigTransaction::TYPE_AUTO_LOGIN)
->setNewValue($request->getInt('autoLogin', 0));
}
foreach ($properties as $key => $value) {
$xactions[] = id(new PhabricatorAuthProviderConfigTransaction())
->setTransactionType(
PhabricatorAuthProviderConfigTransaction::TYPE_PROPERTY)
->setMetadataValue('auth:property', $key)
->setNewValue($value);
}
if ($is_new) {
$config->save();
}
$editor = id(new PhabricatorAuthProviderConfigEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->applyTransactions($config, $xactions);
- if ($provider->hasSetupStep() && $is_new) {
- $id = $config->getID();
- $next_uri = $this->getApplicationURI('config/edit/'.$id.'/');
- } else {
- $next_uri = $this->getApplicationURI();
- }
+ $next_uri = $config->getURI();
return id(new AphrontRedirectResponse())->setURI($next_uri);
}
} else {
$properties = $provider->readFormValuesFromProvider();
$issues = array();
}
if ($is_new) {
if ($provider->hasSetupStep()) {
$button = pht('Next Step');
} else {
$button = pht('Add Provider');
}
$crumb = pht('Add Provider');
$title = pht('Add Auth Provider');
$header_icon = 'fa-plus-square';
$cancel_uri = $this->getApplicationURI('/config/new/');
} else {
$button = pht('Save');
$crumb = pht('Edit Provider');
$title = pht('Edit Auth Provider');
$header_icon = 'fa-pencil';
- $cancel_uri = $this->getApplicationURI();
+ $cancel_uri = $config->getURI();
}
$header = id(new PHUIHeaderView())
->setHeader(pht('%s: %s', $title, $provider->getProviderName()))
->setHeaderIcon($header_icon);
if (!$is_new) {
if ($config->getIsEnabled()) {
$status_name = pht('Enabled');
$status_color = 'green';
$status_icon = 'fa-check';
$header->setStatus($status_icon, $status_color, $status_name);
} else {
$status_name = pht('Disabled');
$status_color = 'indigo';
$status_icon = 'fa-ban';
$header->setStatus($status_icon, $status_color, $status_name);
}
}
$config_name = 'auth.email-domains';
$config_href = '/config/edit/'.$config_name.'/';
$email_domains = PhabricatorEnv::getEnvConfig($config_name);
if ($email_domains) {
$registration_warning = pht(
'Users will only be able to register with a verified email address '.
'at one of the configured [[ %s | %s ]] domains: **%s**',
$config_href,
$config_name,
implode(', ', $email_domains));
} else {
$registration_warning = pht(
"NOTE: Any user who can browse to this install's login page will be ".
"able to register a Phabricator account. To restrict who can register ".
"an account, configure [[ %s | %s ]].",
$config_href,
$config_name);
}
$str_login = array(
phutil_tag('strong', array(), pht('Allow Login:')),
' ',
pht(
'Allow users to log in using this provider. If you disable login, '.
'users can still use account integrations for this provider.'),
);
$str_registration = array(
phutil_tag('strong', array(), pht('Allow Registration:')),
' ',
pht(
'Allow users to register new Phabricator accounts using this '.
'provider. If you disable registration, users can still use this '.
'provider to log in to existing accounts, but will not be able to '.
'create new accounts.'),
);
$str_link = hsprintf(
'<strong>%s:</strong> %s',
pht('Allow Linking Accounts'),
pht(
'Allow users to link account credentials for this provider to '.
'existing Phabricator accounts. There is normally no reason to '.
'disable this unless you are trying to move away from a provider '.
'and want to stop users from creating new account links.'));
$str_unlink = hsprintf(
'<strong>%s:</strong> %s',
pht('Allow Unlinking Accounts'),
pht(
'Allow users to unlink account credentials for this provider from '.
'existing Phabricator accounts. If you disable this, Phabricator '.
'accounts will be permanently bound to provider accounts.'));
$str_trusted_email = hsprintf(
'<strong>%s:</strong> %s',
pht('Trust Email Addresses'),
pht(
'Phabricator will skip email verification for accounts registered '.
'through this provider.'));
$str_auto_login = hsprintf(
'<strong>%s:</strong> %s',
pht('Allow Auto Login'),
pht(
'Phabricator will automatically login with this provider if it is '.
'the only available provider.'));
$form = id(new AphrontFormView())
->setUser($viewer)
+ ->addHiddenInput('provider', $provider_class)
->appendChild(
id(new AphrontFormCheckboxControl())
->setLabel(pht('Allow'))
->addCheckbox(
'allowLogin',
1,
$str_login,
$v_login))
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'allowRegistration',
1,
$str_registration,
$v_registration))
->appendRemarkupInstructions($registration_warning)
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'allowLink',
1,
$str_link,
$v_link))
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'allowUnlink',
1,
$str_unlink,
$v_unlink));
if ($provider->shouldAllowEmailTrustConfiguration()) {
$form->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'trustEmails',
1,
$str_trusted_email,
$v_trust_email));
}
if ($provider->supportsAutoLogin()) {
$form->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'autoLogin',
1,
$str_auto_login,
$v_auto_login));
}
$provider->extendEditForm($request, $form, $properties, $issues);
$form
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($cancel_uri)
->setValue($button));
$help = $provider->getConfigurationHelp();
if ($help) {
$form->appendChild(id(new PHUIFormDividerControl()));
$form->appendRemarkupInstructions($help);
}
$footer = $provider->renderConfigurationFooter();
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($crumb);
$crumbs->setBorder(true);
- $timeline = null;
- if (!$is_new) {
- $timeline = $this->buildTransactionTimeline(
- $config,
- new PhabricatorAuthProviderConfigTransactionQuery());
- $xactions = $timeline->getTransactions();
- foreach ($xactions as $xaction) {
- $xaction->setProvider($provider);
- }
- $timeline->setShouldTerminate(true);
- }
-
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Provider'))
->setFormErrors($errors)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setForm($form);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(
$form_box,
$footer,
- $timeline,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
}
diff --git a/src/applications/auth/controller/config/PhabricatorAuthListController.php b/src/applications/auth/controller/config/PhabricatorAuthListController.php
index bb118d798..f4b05e8ad 100644
--- a/src/applications/auth/controller/config/PhabricatorAuthListController.php
+++ b/src/applications/auth/controller/config/PhabricatorAuthListController.php
@@ -1,142 +1,118 @@
<?php
final class PhabricatorAuthListController
extends PhabricatorAuthProviderConfigController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$configs = id(new PhabricatorAuthProviderConfigQuery())
->setViewer($viewer)
->execute();
$list = new PHUIObjectItemListView();
$can_manage = $this->hasApplicationCapability(
AuthManageProvidersCapability::CAPABILITY);
foreach ($configs as $config) {
$item = new PHUIObjectItemView();
$id = $config->getID();
- $edit_uri = $this->getApplicationURI('config/edit/'.$id.'/');
- $enable_uri = $this->getApplicationURI('config/enable/'.$id.'/');
- $disable_uri = $this->getApplicationURI('config/disable/'.$id.'/');
+ $view_uri = $config->getURI();
$provider = $config->getProvider();
- if ($provider) {
- $name = $provider->getProviderName();
- } else {
- $name = $config->getProviderType().' ('.$config->getProviderClass().')';
- }
-
- $item->setHeader($name);
+ $name = $provider->getProviderName();
- if ($provider) {
- $item->setHref($edit_uri);
- } else {
- $item->addAttribute(pht('Provider Implementation Missing!'));
- }
+ $item
+ ->setHeader($name)
+ ->setHref($view_uri);
- $domain = null;
- if ($provider) {
- $domain = $provider->getProviderDomain();
- if ($domain !== 'self') {
- $item->addAttribute($domain);
- }
+ $domain = $provider->getProviderDomain();
+ if ($domain !== 'self') {
+ $item->addAttribute($domain);
}
if ($config->getShouldAllowRegistration()) {
$item->addAttribute(pht('Allows Registration'));
} else {
$item->addAttribute(pht('Does Not Allow Registration'));
}
if ($config->getIsEnabled()) {
$item->setStatusIcon('fa-check-circle green');
- $item->addAction(
- id(new PHUIListItemView())
- ->setIcon('fa-times')
- ->setHref($disable_uri)
- ->setDisabled(!$can_manage)
- ->addSigil('workflow'));
} else {
$item->setStatusIcon('fa-ban red');
$item->addIcon('fa-ban grey', pht('Disabled'));
- $item->addAction(
- id(new PHUIListItemView())
- ->setIcon('fa-plus')
- ->setHref($enable_uri)
- ->setDisabled(!$can_manage)
- ->addSigil('workflow'));
}
$list->addItem($item);
}
$list->setNoDataString(
pht(
'%s You have not added authentication providers yet. Use "%s" to add '.
'a provider, which will let users register new Phabricator accounts '.
'and log in.',
phutil_tag(
'strong',
array(),
pht('No Providers Configured:')),
phutil_tag(
'a',
array(
'href' => $this->getApplicationURI('config/new/'),
),
pht('Add Authentication Provider'))));
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Login and Registration'));
$crumbs->setBorder(true);
$guidance_context = new PhabricatorAuthProvidersGuidanceContext();
$guidance = id(new PhabricatorGuidanceEngine())
->setViewer($viewer)
->setGuidanceContext($guidance_context)
->newInfoView();
$button = id(new PHUIButtonView())
->setTag('a')
->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE)
->setHref($this->getApplicationURI('config/new/'))
->setIcon('fa-plus')
->setDisabled(!$can_manage)
->setText(pht('Add Provider'));
$list->setFlush(true);
$list = id(new PHUIObjectBoxView())
->setHeaderText(pht('Providers'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($list);
$title = pht('Login and Registration Providers');
$header = id(new PHUIHeaderView())
->setHeader($title)
->setHeaderIcon('fa-key')
->addActionLink($button);
$view = id(new PHUITwoColumnView())
->setHeader($header)
- ->setFooter(array(
- $guidance,
- $list,
- ));
+ ->setFooter(
+ array(
+ $guidance,
+ $list,
+ ));
$nav = $this->newNavigation()
->setCrumbs($crumbs)
->appendChild($view);
$nav->selectFilter('login');
return $this->newPage()
->setTitle($title)
->appendChild($nav);
}
}
diff --git a/src/applications/auth/controller/config/PhabricatorAuthNewController.php b/src/applications/auth/controller/config/PhabricatorAuthNewController.php
index dbd43f9ea..770c43208 100644
--- a/src/applications/auth/controller/config/PhabricatorAuthNewController.php
+++ b/src/applications/auth/controller/config/PhabricatorAuthNewController.php
@@ -1,111 +1,74 @@
<?php
final class PhabricatorAuthNewController
extends PhabricatorAuthProviderConfigController {
public function handleRequest(AphrontRequest $request) {
$this->requireApplicationCapability(
AuthManageProvidersCapability::CAPABILITY);
- $request = $this->getRequest();
- $viewer = $request->getUser();
- $providers = PhabricatorAuthProvider::getAllBaseProviders();
-
- $e_provider = null;
- $errors = array();
-
- if ($request->isFormPost()) {
- $provider_string = $request->getStr('provider');
- if (!strlen($provider_string)) {
- $e_provider = pht('Required');
- $errors[] = pht('You must select an authentication provider.');
- } else {
- $found = false;
- foreach ($providers as $provider) {
- if (get_class($provider) === $provider_string) {
- $found = true;
- break;
- }
- }
- if (!$found) {
- $e_provider = pht('Invalid');
- $errors[] = pht('You must select a valid provider.');
- }
- }
+ $viewer = $this->getViewer();
+ $cancel_uri = $this->getApplicationURI();
- if (!$errors) {
- return id(new AphrontRedirectResponse())->setURI(
- $this->getApplicationURI('/config/new/'.$provider_string.'/'));
- }
- }
-
- $options = id(new AphrontFormRadioButtonControl())
- ->setLabel(pht('Provider'))
- ->setName('provider')
- ->setError($e_provider);
+ $providers = PhabricatorAuthProvider::getAllBaseProviders();
$configured = PhabricatorAuthProvider::getAllProviders();
$configured_classes = array();
foreach ($configured as $configured_provider) {
$configured_classes[get_class($configured_provider)] = true;
}
// Sort providers by login order, and move disabled providers to the
// bottom.
$providers = msort($providers, 'getLoginOrder');
$providers = array_diff_key($providers, $configured_classes) + $providers;
- foreach ($providers as $provider) {
- if (isset($configured_classes[get_class($provider)])) {
- $disabled = true;
- $description = pht('This provider is already configured.');
+ $menu = id(new PHUIObjectItemListView())
+ ->setViewer($viewer)
+ ->setBig(true)
+ ->setFlush(true);
+
+ foreach ($providers as $provider_key => $provider) {
+ $provider_class = get_class($provider);
+
+ $provider_uri = id(new PhutilURI('/config/edit/'))
+ ->replaceQueryParam('provider', $provider_class);
+ $provider_uri = $this->getApplicationURI($provider_uri);
+
+ $already_exists = isset($configured_classes[get_class($provider)]);
+
+ $item = id(new PHUIObjectItemView())
+ ->setHeader($provider->getNameForCreate())
+ ->setImageIcon($provider->newIconView())
+ ->addAttribute($provider->getDescriptionForCreate());
+
+ if (!$already_exists) {
+ $item
+ ->setHref($provider_uri)
+ ->setClickable(true);
} else {
- $disabled = false;
- $description = $provider->getDescriptionForCreate();
+ $item->setDisabled(true);
}
- $options->addButton(
- get_class($provider),
- $provider->getNameForCreate(),
- $description,
- $disabled ? 'disabled' : null,
- $disabled);
- }
- $form = id(new AphrontFormView())
- ->setUser($viewer)
- ->appendChild($options)
- ->appendChild(
- id(new AphrontFormSubmitControl())
- ->addCancelButton($this->getApplicationURI())
- ->setValue(pht('Continue')));
-
- $form_box = id(new PHUIObjectBoxView())
- ->setHeaderText(pht('Provider'))
- ->setFormErrors($errors)
- ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
- ->setForm($form);
-
- $crumbs = $this->buildApplicationCrumbs();
- $crumbs->addTextCrumb(pht('Add Provider'));
- $crumbs->setBorder(true);
-
- $title = pht('Add Auth Provider');
-
- $header = id(new PHUIHeaderView())
- ->setHeader($title)
- ->setHeaderIcon('fa-plus-square');
-
- $view = id(new PHUITwoColumnView())
- ->setHeader($header)
- ->setFooter(array(
- $form_box,
- ));
-
- return $this->newPage()
- ->setTitle($title)
- ->setCrumbs($crumbs)
- ->appendChild($view);
+ if ($already_exists) {
+ $messages = array();
+ $messages[] = pht('You already have a provider of this type.');
+
+ $info = id(new PHUIInfoView())
+ ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
+ ->setErrors($messages);
+
+ $item->appendChild($info);
+ }
+
+ $menu->addItem($item);
+ }
+ return $this->newDialog()
+ ->setTitle(pht('Add Auth Provider'))
+ ->setWidth(AphrontDialogView::WIDTH_FORM)
+ ->appendChild($menu)
+ ->addCancelButton($cancel_uri);
}
}
diff --git a/src/applications/auth/controller/config/PhabricatorAuthProviderViewController.php b/src/applications/auth/controller/config/PhabricatorAuthProviderViewController.php
new file mode 100644
index 000000000..532744001
--- /dev/null
+++ b/src/applications/auth/controller/config/PhabricatorAuthProviderViewController.php
@@ -0,0 +1,119 @@
+<?php
+
+final class PhabricatorAuthProviderViewController
+ extends PhabricatorAuthProviderConfigController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $this->requireApplicationCapability(
+ AuthManageProvidersCapability::CAPABILITY);
+
+ $viewer = $this->getViewer();
+ $id = $request->getURIData('id');
+
+ $config = id(new PhabricatorAuthProviderConfigQuery())
+ ->setViewer($viewer)
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->withIDs(array($id))
+ ->executeOne();
+ if (!$config) {
+ return new Aphront404Response();
+ }
+
+ $header = $this->buildHeaderView($config);
+ $properties = $this->buildPropertiesView($config);
+ $curtain = $this->buildCurtain($config);
+
+ $timeline = $this->buildTransactionTimeline(
+ $config,
+ new PhabricatorAuthProviderConfigTransactionQuery());
+ $timeline->setShouldTerminate(true);
+
+ $view = id(new PHUITwoColumnView())
+ ->setHeader($header)
+ ->setCurtain($curtain)
+ ->addPropertySection(pht('Details'), $properties)
+ ->setMainColumn($timeline);
+
+ $crumbs = $this->buildApplicationCrumbs()
+ ->addTextCrumb($config->getObjectName())
+ ->setBorder(true);
+
+ return $this->newPage()
+ ->setTitle(pht('Auth Provider: %s', $config->getDisplayName()))
+ ->setCrumbs($crumbs)
+ ->appendChild($view);
+ }
+
+ private function buildHeaderView(PhabricatorAuthProviderConfig $config) {
+ $viewer = $this->getViewer();
+
+ $view = id(new PHUIHeaderView())
+ ->setViewer($viewer)
+ ->setHeader($config->getDisplayName());
+
+ if ($config->getIsEnabled()) {
+ $view->setStatus('fa-check', 'bluegrey', pht('Enabled'));
+ } else {
+ $view->setStatus('fa-ban', 'red', pht('Disabled'));
+ }
+
+ return $view;
+ }
+
+ private function buildCurtain(PhabricatorAuthProviderConfig $config) {
+ $viewer = $this->getViewer();
+ $id = $config->getID();
+
+ $can_edit = PhabricatorPolicyFilter::hasCapability(
+ $viewer,
+ $config,
+ PhabricatorPolicyCapability::CAN_EDIT);
+
+ $curtain = $this->newCurtainView($config);
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setName(pht('Edit Auth Provider'))
+ ->setIcon('fa-pencil')
+ ->setHref($this->getApplicationURI("config/edit/{$id}/"))
+ ->setDisabled(!$can_edit)
+ ->setWorkflow(!$can_edit));
+
+ if ($config->getIsEnabled()) {
+ $disable_uri = $this->getApplicationURI('config/disable/'.$id.'/');
+ $disable_icon = 'fa-ban';
+ $disable_text = pht('Disable Provider');
+ } else {
+ $disable_uri = $this->getApplicationURI('config/enable/'.$id.'/');
+ $disable_icon = 'fa-check';
+ $disable_text = pht('Enable Provider');
+ }
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setName($disable_text)
+ ->setIcon($disable_icon)
+ ->setHref($disable_uri)
+ ->setDisabled(!$can_edit)
+ ->setWorkflow(true));
+
+ return $curtain;
+ }
+
+ private function buildPropertiesView(PhabricatorAuthProviderConfig $config) {
+ $viewer = $this->getViewer();
+
+ $view = id(new PHUIPropertyListView())
+ ->setViewer($viewer);
+
+ $view->addProperty(
+ pht('Provider Type'),
+ $config->getProvider()->getProviderName());
+
+ return $view;
+ }
+}
diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthChallengeStatusController.php b/src/applications/auth/controller/mfa/PhabricatorAuthChallengeStatusController.php
new file mode 100644
index 000000000..3fbffabc8
--- /dev/null
+++ b/src/applications/auth/controller/mfa/PhabricatorAuthChallengeStatusController.php
@@ -0,0 +1,46 @@
+<?php
+
+final class PhabricatorAuthChallengeStatusController
+ extends PhabricatorAuthController {
+
+ public function shouldAllowPartialSessions() {
+ // We expect that users may request the status of an MFA challenge when
+ // they hit the session upgrade gate on login.
+ return true;
+ }
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+ $id = $request->getURIData('id');
+ $now = PhabricatorTime::getNow();
+
+ $result = new PhabricatorAuthChallengeUpdate();
+
+ $challenge = id(new PhabricatorAuthChallengeQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($id))
+ ->withUserPHIDs(array($viewer->getPHID()))
+ ->withChallengeTTLBetween($now, null)
+ ->executeOne();
+ if ($challenge) {
+ $config = id(new PhabricatorAuthFactorConfigQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($challenge->getFactorPHID()))
+ ->executeOne();
+ if ($config) {
+ $provider = $config->getFactorProvider();
+ $factor = $provider->getFactor();
+
+ $result = $factor->newChallengeStatusView(
+ $config,
+ $provider,
+ $viewer,
+ $challenge);
+ }
+ }
+
+ return id(new AphrontAjaxResponse())
+ ->setContent($result->newContent());
+ }
+
+}
diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php
index a8d87e2ea..a1636396a 100644
--- a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php
+++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php
@@ -1,80 +1,80 @@
<?php
final class PhabricatorAuthFactorProviderEditController
extends PhabricatorAuthFactorProviderController {
public function handleRequest(AphrontRequest $request) {
$this->requireApplicationCapability(
AuthManageProvidersCapability::CAPABILITY);
$engine = id(new PhabricatorAuthFactorProviderEditEngine())
->setController($this);
$id = $request->getURIData('id');
if (!$id) {
$factor_key = $request->getStr('providerFactorKey');
$map = PhabricatorAuthFactor::getAllFactors();
$factor = idx($map, $factor_key);
if (!$factor) {
return $this->buildFactorSelectionResponse();
}
$engine
->addContextParameter('providerFactorKey', $factor_key)
->setProviderFactor($factor);
}
return $engine->buildResponse();
}
private function buildFactorSelectionResponse() {
$request = $this->getRequest();
$viewer = $this->getViewer();
$cancel_uri = $this->getApplicationURI('mfa/');
$factors = PhabricatorAuthFactor::getAllFactors();
$menu = id(new PHUIObjectItemListView())
->setUser($viewer)
->setBig(true)
->setFlush(true);
$factors = msortv($factors, 'newSortVector');
foreach ($factors as $factor_key => $factor) {
$factor_uri = id(new PhutilURI('/mfa/edit/'))
- ->setQueryParam('providerFactorKey', $factor_key);
+ ->replaceQueryParam('providerFactorKey', $factor_key);
$factor_uri = $this->getApplicationURI($factor_uri);
$is_enabled = $factor->canCreateNewProvider();
$item = id(new PHUIObjectItemView())
->setHeader($factor->getFactorName())
->setImageIcon($factor->newIconView())
->addAttribute($factor->getFactorCreateHelp());
if ($is_enabled) {
$item
->setHref($factor_uri)
->setClickable(true);
} else {
$item->setDisabled(true);
}
$create_description = $factor->getProviderCreateDescription();
if ($create_description) {
$item->appendChild($create_description);
}
$menu->addItem($item);
}
return $this->newDialog()
->setTitle(pht('Choose Provider Type'))
->appendChild($menu)
->addCancelButton($cancel_uri);
}
}
diff --git a/src/applications/auth/engine/PhabricatorAuthInviteEngine.php b/src/applications/auth/engine/PhabricatorAuthInviteEngine.php
index f1cb45483..70fc03345 100644
--- a/src/applications/auth/engine/PhabricatorAuthInviteEngine.php
+++ b/src/applications/auth/engine/PhabricatorAuthInviteEngine.php
@@ -1,256 +1,256 @@
<?php
/**
* This class does an unusual amount of flow control via exceptions. The intent
* is to make the workflows highly testable, because this code is high-stakes
* and difficult to test.
*/
final class PhabricatorAuthInviteEngine extends Phobject {
private $viewer;
private $userHasConfirmedVerify;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
if (!$this->viewer) {
throw new PhutilInvalidStateException('setViewer');
}
return $this->viewer;
}
public function setUserHasConfirmedVerify($confirmed) {
$this->userHasConfirmedVerify = $confirmed;
return $this;
}
private function shouldVerify() {
return $this->userHasConfirmedVerify;
}
public function processInviteCode($code) {
$viewer = $this->getViewer();
$invite = id(new PhabricatorAuthInviteQuery())
->setViewer($viewer)
->withVerificationCodes(array($code))
->executeOne();
if (!$invite) {
throw id(new PhabricatorAuthInviteInvalidException(
pht('Bad Invite Code'),
pht(
'The invite code in the link you clicked is invalid. Check that '.
'you followed the link correctly.')))
->setCancelButtonURI('/')
->setCancelButtonText(pht('Curses!'));
}
$accepted_phid = $invite->getAcceptedByPHID();
if ($accepted_phid) {
if ($accepted_phid == $viewer->getPHID()) {
throw id(new PhabricatorAuthInviteInvalidException(
pht('Already Accepted'),
pht(
'You have already accepted this invitation.')))
->setCancelButtonURI('/')
->setCancelButtonText(pht('Awesome'));
} else {
throw id(new PhabricatorAuthInviteInvalidException(
pht('Already Accepted'),
pht(
'The invite code in the link you clicked has already '.
'been accepted.')))
->setCancelButtonURI('/')
->setCancelButtonText(pht('Continue'));
}
}
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$invite->getEmailAddress());
if ($viewer->isLoggedIn()) {
$this->handleLoggedInInvite($invite, $viewer, $email);
}
if ($email) {
$other_user = $this->loadUserForEmail($email);
if ($email->getIsVerified()) {
throw id(new PhabricatorAuthInviteLoginException(
pht('Already Registered'),
pht(
'The email address you just clicked a link from is already '.
'verified and associated with a registered account (%s). Log '.
'in to continue.',
phutil_tag('strong', array(), $other_user->getName()))))
->setCancelButtonText(pht('Log In'))
->setCancelButtonURI($this->getLoginURI());
} else if ($email->getIsPrimary()) {
throw id(new PhabricatorAuthInviteLoginException(
pht('Already Registered'),
pht(
'The email address you just clicked a link from is already '.
'the primary email address for a registered account (%s). Log '.
'in to continue.',
phutil_tag('strong', array(), $other_user->getName()))))
->setCancelButtonText(pht('Log In'))
->setCancelButtonURI($this->getLoginURI());
} else if (!$this->shouldVerify()) {
throw id(new PhabricatorAuthInviteVerifyException(
pht('Already Associated'),
pht(
'The email address you just clicked a link from is already '.
'associated with a registered account (%s), but is not '.
'verified. Log in to that account to continue. If you can not '.
'log in, you can register a new account.',
phutil_tag('strong', array(), $other_user->getName()))))
->setCancelButtonText(pht('Log In'))
->setCancelButtonURI($this->getLoginURI())
->setSubmitButtonText(pht('Register New Account'));
} else {
// NOTE: The address is not verified and not a primary address, so
// we will eventually steal it if the user completes registration.
}
}
// The invite and email address are OK, but the user needs to register.
return $invite;
}
private function handleLoggedInInvite(
PhabricatorAuthInvite $invite,
PhabricatorUser $viewer,
PhabricatorUserEmail $email = null) {
if ($email && ($email->getUserPHID() !== $viewer->getPHID())) {
$other_user = $this->loadUserForEmail($email);
if ($email->getIsVerified()) {
throw id(new PhabricatorAuthInviteAccountException(
pht('Wrong Account'),
pht(
'You are logged in as %s, but the email address you just '.
'clicked a link from is already verified and associated '.
'with another account (%s). Switch accounts, then try again.',
phutil_tag('strong', array(), $viewer->getUsername()),
phutil_tag('strong', array(), $other_user->getName()))))
->setSubmitButtonText(pht('Log Out'))
->setSubmitButtonURI($this->getLogoutURI())
->setCancelButtonURI('/');
} else if ($email->getIsPrimary()) {
// NOTE: We never steal primary addresses from other accounts, even
// if they are unverified. This would leave the other account with
// no address. Users can use password recovery to access the other
// account if they really control the address.
throw id(new PhabricatorAuthInviteAccountException(
- pht('Wrong Acount'),
+ pht('Wrong Account'),
pht(
'You are logged in as %s, but the email address you just '.
'clicked a link from is already the primary email address '.
'for another account (%s). Switch accounts, then try again.',
phutil_tag('strong', array(), $viewer->getUsername()),
phutil_tag('strong', array(), $other_user->getName()))))
->setSubmitButtonText(pht('Log Out'))
->setSubmitButtonURI($this->getLogoutURI())
->setCancelButtonURI('/');
} else if (!$this->shouldVerify()) {
throw id(new PhabricatorAuthInviteVerifyException(
pht('Verify Email'),
pht(
'You are logged in as %s, but the email address (%s) you just '.
'clicked a link from is already associated with another '.
'account (%s). You can log out to switch accounts, or verify '.
'the address and attach it to your current account. Attach '.
'email address %s to user account %s?',
phutil_tag('strong', array(), $viewer->getUsername()),
phutil_tag('strong', array(), $invite->getEmailAddress()),
phutil_tag('strong', array(), $other_user->getName()),
phutil_tag('strong', array(), $invite->getEmailAddress()),
phutil_tag('strong', array(), $viewer->getUsername()))))
->setSubmitButtonText(
pht(
'Verify %s',
$invite->getEmailAddress()))
->setCancelButtonText(pht('Log Out'))
->setCancelButtonURI($this->getLogoutURI());
}
}
if (!$email) {
$email = id(new PhabricatorUserEmail())
->setAddress($invite->getEmailAddress())
->setIsVerified(0)
->setIsPrimary(0);
}
if (!$email->getIsVerified()) {
// We're doing this check here so that we can verify the address if
// it's already attached to the viewer's account, just not verified.
if (!$this->shouldVerify()) {
throw id(new PhabricatorAuthInviteVerifyException(
pht('Verify Email'),
pht(
'Verify this email address (%s) and attach it to your '.
'account (%s)?',
phutil_tag('strong', array(), $invite->getEmailAddress()),
phutil_tag('strong', array(), $viewer->getUsername()))))
->setSubmitButtonText(
pht(
'Verify %s',
$invite->getEmailAddress()))
->setCancelButtonURI('/');
}
$editor = id(new PhabricatorUserEditor())
->setActor($viewer);
// If this is a new email, add it to the user's account.
if (!$email->getUserPHID()) {
$editor->addEmail($viewer, $email);
}
// If another user added this email (but has not verified it),
// take it from them.
$editor->reassignEmail($viewer, $email);
$editor->verifyEmail($viewer, $email);
}
$invite->setAcceptedByPHID($viewer->getPHID());
$invite->save();
// If we make it here, the user was already logged in with the email
// address attached to their account and verified, or we attached it to
// their account (if it was not already attached) and verified it.
throw new PhabricatorAuthInviteRegisteredException();
}
private function loadUserForEmail(PhabricatorUserEmail $email) {
$user = id(new PhabricatorHandleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($email->getUserPHID()))
->executeOne();
if (!$user) {
throw new Exception(
pht(
'Email record ("%s") has bad associated user PHID ("%s").',
$email->getAddress(),
$email->getUserPHID()));
}
return $user;
}
private function getLoginURI() {
return '/auth/start/';
}
private function getLogoutURI() {
return '/logout/';
}
}
diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php
index c05280522..38ae2201b 100644
--- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php
+++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php
@@ -1,1163 +1,1170 @@
<?php
/**
*
* @task use Using Sessions
* @task new Creating Sessions
* @task hisec High Security
* @task partial Partial Sessions
* @task onetime One Time Login URIs
* @task cache User Cache
*/
final class PhabricatorAuthSessionEngine extends Phobject {
/**
* Session issued to normal users after they login through a standard channel.
* Associates the client with a standard user identity.
*/
const KIND_USER = 'U';
/**
* Session issued to users who login with some sort of credentials but do not
* have full accounts. These are sometimes called "grey users".
*
* TODO: We do not currently issue these sessions, see T4310.
*/
const KIND_EXTERNAL = 'X';
/**
* Session issued to logged-out users which has no real identity information.
* Its purpose is to protect logged-out users from CSRF.
*/
const KIND_ANONYMOUS = 'A';
/**
* Session kind isn't known.
*/
const KIND_UNKNOWN = '?';
const ONETIME_RECOVER = 'recover';
const ONETIME_RESET = 'reset';
const ONETIME_WELCOME = 'welcome';
const ONETIME_USERNAME = 'rename';
private $workflowKey;
private $request;
public function setWorkflowKey($workflow_key) {
$this->workflowKey = $workflow_key;
return $this;
}
public function getWorkflowKey() {
// TODO: A workflow key should become required in order to issue an MFA
// challenge, but allow things to keep working for now until we can update
// callsites.
if ($this->workflowKey === null) {
return 'legacy';
}
return $this->workflowKey;
}
public function getRequest() {
return $this->request;
}
/**
* Get the session kind (e.g., anonymous, user, external account) from a
* session token. Returns a `KIND_` constant.
*
* @param string Session token.
* @return const Session kind constant.
*/
public static function getSessionKindFromToken($session_token) {
if (strpos($session_token, '/') === false) {
// Old-style session, these are all user sessions.
return self::KIND_USER;
}
list($kind, $key) = explode('/', $session_token, 2);
switch ($kind) {
case self::KIND_ANONYMOUS:
case self::KIND_USER:
case self::KIND_EXTERNAL:
return $kind;
default:
return self::KIND_UNKNOWN;
}
}
/**
* Load the user identity associated with a session of a given type,
* identified by token.
*
* When the user presents a session token to an API, this method verifies
* it is of the correct type and loads the corresponding identity if the
* session exists and is valid.
*
* NOTE: `$session_type` is the type of session that is required by the
* loading context. This prevents use of a Conduit sesssion as a Web
* session, for example.
*
* @param const The type of session to load.
* @param string The session token.
* @return PhabricatorUser|null
* @task use
*/
public function loadUserForSession($session_type, $session_token) {
$session_kind = self::getSessionKindFromToken($session_token);
switch ($session_kind) {
case self::KIND_ANONYMOUS:
// Don't bother trying to load a user for an anonymous session, since
// neither the session nor the user exist.
return null;
case self::KIND_UNKNOWN:
// If we don't know what kind of session this is, don't go looking for
// it.
return null;
case self::KIND_USER:
break;
case self::KIND_EXTERNAL:
// TODO: Implement these (T4310).
return null;
}
$session_table = new PhabricatorAuthSession();
$user_table = new PhabricatorUser();
$conn = $session_table->establishConnection('r');
// TODO: See T13225. We're moving sessions to a more modern digest
// algorithm, but still accept older cookies for compatibility.
$session_key = PhabricatorAuthSession::newSessionDigest(
new PhutilOpaqueEnvelope($session_token));
$weak_key = PhabricatorHash::weakDigest($session_token);
$cache_parts = $this->getUserCacheQueryParts($conn);
list($cache_selects, $cache_joins, $cache_map, $types_map) = $cache_parts;
$info = queryfx_one(
$conn,
'SELECT
s.id AS s_id,
s.phid AS s_phid,
s.sessionExpires AS s_sessionExpires,
s.sessionStart AS s_sessionStart,
s.highSecurityUntil AS s_highSecurityUntil,
s.isPartial AS s_isPartial,
s.signedLegalpadDocuments as s_signedLegalpadDocuments,
IF(s.sessionKey = %P, 1, 0) as s_weak,
u.*
%Q
FROM %R u JOIN %R s ON u.phid = s.userPHID
AND s.type = %s AND s.sessionKey IN (%P, %P) %Q',
new PhutilOpaqueEnvelope($weak_key),
$cache_selects,
$user_table,
$session_table,
$session_type,
new PhutilOpaqueEnvelope($session_key),
new PhutilOpaqueEnvelope($weak_key),
$cache_joins);
if (!$info) {
return null;
}
// TODO: Remove this, see T13225.
$is_weak = (bool)$info['s_weak'];
unset($info['s_weak']);
$session_dict = array(
'userPHID' => $info['phid'],
'sessionKey' => $session_key,
'type' => $session_type,
);
$cache_raw = array_fill_keys($cache_map, null);
foreach ($info as $key => $value) {
if (strncmp($key, 's_', 2) === 0) {
unset($info[$key]);
$session_dict[substr($key, 2)] = $value;
continue;
}
if (isset($cache_map[$key])) {
unset($info[$key]);
$cache_raw[$cache_map[$key]] = $value;
continue;
}
}
$user = $user_table->loadFromArray($info);
$cache_raw = $this->filterRawCacheData($user, $types_map, $cache_raw);
$user->attachRawCacheData($cache_raw);
switch ($session_type) {
case PhabricatorAuthSession::TYPE_WEB:
// Explicitly prevent bots and mailing lists from establishing web
// sessions. It's normally impossible to attach authentication to these
// accounts, and likewise impossible to generate sessions, but it's
// technically possible that a session could exist in the database. If
// one does somehow, refuse to load it.
if (!$user->canEstablishWebSessions()) {
return null;
}
break;
}
$session = id(new PhabricatorAuthSession())->loadFromArray($session_dict);
$this->extendSession($session);
// TODO: Remove this, see T13225.
if ($is_weak) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$conn_w = $session_table->establishConnection('w');
queryfx(
$conn_w,
'UPDATE %T SET sessionKey = %P WHERE id = %d',
$session->getTableName(),
new PhutilOpaqueEnvelope($session_key),
$session->getID());
unset($unguarded);
}
$user->attachSession($session);
return $user;
}
/**
* Issue a new session key for a given identity. Phabricator supports
* different types of sessions (like "web" and "conduit") and each session
* type may have multiple concurrent sessions (this allows a user to be
* logged in on multiple browsers at the same time, for instance).
*
* Note that this method is transport-agnostic and does not set cookies or
* issue other types of tokens, it ONLY generates a new session key.
*
* You can configure the maximum number of concurrent sessions for various
* session types in the Phabricator configuration.
*
* @param const Session type constant (see
* @{class:PhabricatorAuthSession}).
* @param phid|null Identity to establish a session for, usually a user
* PHID. With `null`, generates an anonymous session.
* @param bool True to issue a partial session.
* @return string Newly generated session key.
*/
public function establishSession($session_type, $identity_phid, $partial) {
// Consume entropy to generate a new session key, forestalling the eventual
// heat death of the universe.
$session_key = Filesystem::readRandomCharacters(40);
if ($identity_phid === null) {
return self::KIND_ANONYMOUS.'/'.$session_key;
}
$session_table = new PhabricatorAuthSession();
$conn_w = $session_table->establishConnection('w');
// This has a side effect of validating the session type.
$session_ttl = PhabricatorAuthSession::getSessionTypeTTL(
$session_type,
$partial);
$digest_key = PhabricatorAuthSession::newSessionDigest(
new PhutilOpaqueEnvelope($session_key));
// Logging-in users don't have CSRF stuff yet, so we have to unguard this
// write.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
id(new PhabricatorAuthSession())
->setUserPHID($identity_phid)
->setType($session_type)
->setSessionKey($digest_key)
->setSessionStart(time())
->setSessionExpires(time() + $session_ttl)
->setIsPartial($partial ? 1 : 0)
->setSignedLegalpadDocuments(0)
->save();
$log = PhabricatorUserLog::initializeNewLog(
null,
$identity_phid,
($partial
? PhabricatorUserLog::ACTION_LOGIN_PARTIAL
: PhabricatorUserLog::ACTION_LOGIN));
$log->setDetails(
array(
'session_type' => $session_type,
));
$log->setSession($digest_key);
$log->save();
unset($unguarded);
$info = id(new PhabricatorAuthSessionInfo())
->setSessionType($session_type)
->setIdentityPHID($identity_phid)
->setIsPartial($partial);
$extensions = PhabricatorAuthSessionEngineExtension::getAllExtensions();
foreach ($extensions as $extension) {
$extension->didEstablishSession($info);
}
return $session_key;
}
/**
* Terminate all of a user's login sessions.
*
* This is used when users change passwords, linked accounts, or add
* multifactor authentication.
*
* @param PhabricatorUser User whose sessions should be terminated.
* @param string|null Optionally, one session to keep. Normally, the current
* login session.
*
* @return void
*/
public function terminateLoginSessions(
PhabricatorUser $user,
PhutilOpaqueEnvelope $except_session = null) {
$sessions = id(new PhabricatorAuthSessionQuery())
->setViewer($user)
->withIdentityPHIDs(array($user->getPHID()))
->execute();
if ($except_session !== null) {
$except_session = PhabricatorAuthSession::newSessionDigest(
$except_session);
}
foreach ($sessions as $key => $session) {
if ($except_session !== null) {
$is_except = phutil_hashes_are_identical(
$session->getSessionKey(),
$except_session);
if ($is_except) {
continue;
}
}
$session->delete();
}
}
public function logoutSession(
PhabricatorUser $user,
PhabricatorAuthSession $session) {
$log = PhabricatorUserLog::initializeNewLog(
$user,
$user->getPHID(),
PhabricatorUserLog::ACTION_LOGOUT);
$log->save();
$extensions = PhabricatorAuthSessionEngineExtension::getAllExtensions();
foreach ($extensions as $extension) {
$extension->didLogout($user, array($session));
}
$session->delete();
}
/* -( High Security )------------------------------------------------------ */
/**
* Require the user respond to a high security (MFA) check.
*
* This method differs from @{method:requireHighSecuritySession} in that it
* does not upgrade the user's session as a side effect. This method is
* appropriate for one-time checks.
*
* @param PhabricatorUser User whose session needs to be in high security.
* @param AphrontReqeust Current request.
* @param string URI to return the user to if they cancel.
* @return PhabricatorAuthHighSecurityToken Security token.
* @task hisec
*/
public function requireHighSecurityToken(
PhabricatorUser $viewer,
AphrontRequest $request,
$cancel_uri) {
return $this->newHighSecurityToken(
$viewer,
$request,
$cancel_uri,
false,
false);
}
/**
* Require high security, or prompt the user to enter high security.
*
* If the user's session is in high security, this method will return a
* token. Otherwise, it will throw an exception which will eventually
* be converted into a multi-factor authentication workflow.
*
* This method upgrades the user's session to high security for a short
* period of time, and is appropriate if you anticipate they may need to
* take multiple high security actions. To perform a one-time check instead,
* use @{method:requireHighSecurityToken}.
*
* @param PhabricatorUser User whose session needs to be in high security.
* @param AphrontReqeust Current request.
* @param string URI to return the user to if they cancel.
* @param bool True to jump partial sessions directly into high
* security instead of just upgrading them to full
* sessions.
* @return PhabricatorAuthHighSecurityToken Security token.
* @task hisec
*/
public function requireHighSecuritySession(
PhabricatorUser $viewer,
AphrontRequest $request,
$cancel_uri,
$jump_into_hisec = false) {
return $this->newHighSecurityToken(
$viewer,
$request,
$cancel_uri,
$jump_into_hisec,
true);
}
private function newHighSecurityToken(
PhabricatorUser $viewer,
AphrontRequest $request,
$cancel_uri,
$jump_into_hisec,
$upgrade_session) {
if (!$viewer->hasSession()) {
throw new Exception(
pht('Requiring a high-security session from a user with no session!'));
}
// TODO: If a user answers a "requireHighSecurityToken()" prompt and hits
// a "requireHighSecuritySession()" prompt a short time later, the one-shot
// token should be good enough to upgrade the session.
$session = $viewer->getSession();
// Check if the session is already in high security mode.
$token = $this->issueHighSecurityToken($session);
if ($token) {
return $token;
}
// Load the multi-factor auth sources attached to this account. Note that
// we order factors from oldest to newest, which is not the default query
// ordering but makes the greatest sense in context.
$factors = id(new PhabricatorAuthFactorConfigQuery())
->setViewer($viewer)
->withUserPHIDs(array($viewer->getPHID()))
->withFactorProviderStatuses(
array(
PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
PhabricatorAuthFactorProviderStatus::STATUS_DEPRECATED,
))
->execute();
// Sort factors in the same order that they appear in on the Settings
// panel. This means that administrators changing provider statuses may
// change the order of prompts for users, but the alternative is that the
// Settings panel order disagrees with the prompt order, which seems more
// disruptive.
$factors = msort($factors, 'newSortVector');
// If the account has no associated multi-factor auth, just issue a token
// without putting the session into high security mode. This is generally
// easier for users. A minor but desirable side effect is that when a user
// adds an auth factor, existing sessions won't get a free pass into hisec,
// since they never actually got marked as hisec.
if (!$factors) {
return $this->issueHighSecurityToken($session, true);
}
$this->request = $request;
foreach ($factors as $factor) {
$factor->setSessionEngine($this);
}
// Check for a rate limit without awarding points, so the user doesn't
// get partway through the workflow only to get blocked.
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorAuthTryFactorAction(),
0);
$now = PhabricatorTime::getNow();
// We need to do challenge validation first, since this happens whether you
// submitted responses or not. You can't get a "bad response" error before
// you actually submit a response, but you can get a "wait, we can't
// issue a challenge yet" response. Load all issued challenges which are
// currently valid.
$challenges = id(new PhabricatorAuthChallengeQuery())
->setViewer($viewer)
->withFactorPHIDs(mpull($factors, 'getPHID'))
->withUserPHIDs(array($viewer->getPHID()))
->withChallengeTTLBetween($now, null)
->execute();
PhabricatorAuthChallenge::newChallengeResponsesFromRequest(
$challenges,
$request);
$challenge_map = mgroup($challenges, 'getFactorPHID');
$validation_results = array();
$ok = true;
// Validate each factor against issued challenges. For example, this
// prevents you from receiving or responding to a TOTP challenge if another
// challenge was recently issued to a different session.
foreach ($factors as $factor) {
$factor_phid = $factor->getPHID();
$issued_challenges = idx($challenge_map, $factor_phid, array());
$provider = $factor->getFactorProvider();
$impl = $provider->getFactor();
$new_challenges = $impl->getNewIssuedChallenges(
$factor,
$viewer,
$issued_challenges);
// NOTE: We may get a list of challenges back, or may just get an early
// result. For example, this can happen on an SMS factor if all SMS
// mailers have been disabled.
if ($new_challenges instanceof PhabricatorAuthFactorResult) {
$result = $new_challenges;
if (!$result->getIsValid()) {
$ok = false;
}
$validation_results[$factor_phid] = $result;
$challenge_map[$factor_phid] = $issued_challenges;
continue;
}
foreach ($new_challenges as $new_challenge) {
$issued_challenges[] = $new_challenge;
}
$challenge_map[$factor_phid] = $issued_challenges;
if (!$issued_challenges) {
continue;
}
$result = $impl->getResultFromIssuedChallenges(
$factor,
$viewer,
$issued_challenges);
if (!$result) {
continue;
}
if (!$result->getIsValid()) {
$ok = false;
}
$validation_results[$factor_phid] = $result;
}
if ($request->isHTTPPost()) {
$request->validateCSRF();
if ($request->getExists(AphrontRequest::TYPE_HISEC)) {
// Limit factor verification rates to prevent brute force attacks.
$any_attempt = false;
foreach ($factors as $factor) {
$factor_phid = $factor->getPHID();
$provider = $factor->getFactorProvider();
$impl = $provider->getFactor();
// If we already have a result (normally "wait..."), we won't try
// to validate whatever the user submitted, so this doesn't count as
// an attempt for rate limiting purposes.
if (isset($validation_results[$factor_phid])) {
continue;
}
if ($impl->getRequestHasChallengeResponse($factor, $request)) {
$any_attempt = true;
break;
}
}
if ($any_attempt) {
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorAuthTryFactorAction(),
1);
}
foreach ($factors as $factor) {
$factor_phid = $factor->getPHID();
// If we already have a validation result from previously issued
// challenges, skip validating this factor.
if (isset($validation_results[$factor_phid])) {
continue;
}
$issued_challenges = idx($challenge_map, $factor_phid, array());
$provider = $factor->getFactorProvider();
$impl = $provider->getFactor();
$validation_result = $impl->getResultFromChallengeResponse(
$factor,
$viewer,
$request,
$issued_challenges);
if (!$validation_result->getIsValid()) {
$ok = false;
}
$validation_results[$factor_phid] = $validation_result;
}
if ($ok) {
// We're letting you through, so mark all the challenges you
// responded to as completed. These challenges can never be used
// again, even by the same session and workflow: you can't use the
// same response to take two different actions, even if those actions
// are of the same type.
foreach ($validation_results as $validation_result) {
$challenge = $validation_result->getAnsweredChallenge()
->markChallengeAsCompleted();
}
// Give the user a credit back for a successful factor verification.
if ($any_attempt) {
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorAuthTryFactorAction(),
-1);
}
if ($session->getIsPartial() && !$jump_into_hisec) {
// If we have a partial session and are not jumping directly into
// hisec, just issue a token without putting it in high security
// mode.
return $this->issueHighSecurityToken($session, true);
}
// If we aren't upgrading the session itself, just issue a token.
if (!$upgrade_session) {
return $this->issueHighSecurityToken($session, true);
}
$until = time() + phutil_units('15 minutes in seconds');
$session->setHighSecurityUntil($until);
queryfx(
$session->establishConnection('w'),
'UPDATE %T SET highSecurityUntil = %d WHERE id = %d',
$session->getTableName(),
$until,
$session->getID());
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$viewer->getPHID(),
PhabricatorUserLog::ACTION_ENTER_HISEC);
$log->save();
} else {
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$viewer->getPHID(),
PhabricatorUserLog::ACTION_FAIL_HISEC);
$log->save();
}
}
}
$token = $this->issueHighSecurityToken($session);
if ($token) {
return $token;
}
// If we don't have a validation result for some factors yet, fill them
// in with an empty result so form rendering doesn't have to care if the
// results exist or not. This happens when you first load the form and have
// not submitted any responses yet.
foreach ($factors as $factor) {
$factor_phid = $factor->getPHID();
if (isset($validation_results[$factor_phid])) {
continue;
}
- $validation_results[$factor_phid] = new PhabricatorAuthFactorResult();
+
+ $issued_challenges = idx($challenge_map, $factor_phid, array());
+
+ $validation_results[$factor_phid] = $impl->getResultForPrompt(
+ $factor,
+ $viewer,
+ $request,
+ $issued_challenges);
}
throw id(new PhabricatorAuthHighSecurityRequiredException())
->setCancelURI($cancel_uri)
->setIsSessionUpgrade($upgrade_session)
->setFactors($factors)
->setFactorValidationResults($validation_results);
}
/**
* Issue a high security token for a session, if authorized.
*
* @param PhabricatorAuthSession Session to issue a token for.
* @param bool Force token issue.
* @return PhabricatorAuthHighSecurityToken|null Token, if authorized.
* @task hisec
*/
private function issueHighSecurityToken(
PhabricatorAuthSession $session,
$force = false) {
if ($session->isHighSecuritySession() || $force) {
return new PhabricatorAuthHighSecurityToken();
}
return null;
}
/**
* Render a form for providing relevant multi-factor credentials.
*
* @param PhabricatorUser Viewing user.
* @param AphrontRequest Current request.
* @return AphrontFormView Renderable form.
* @task hisec
*/
public function renderHighSecurityForm(
array $factors,
array $validation_results,
PhabricatorUser $viewer,
AphrontRequest $request) {
assert_instances_of($validation_results, 'PhabricatorAuthFactorResult');
$form = id(new AphrontFormView())
->setUser($viewer)
->appendRemarkupInstructions('');
$answered = array();
foreach ($factors as $factor) {
$result = $validation_results[$factor->getPHID()];
$provider = $factor->getFactorProvider();
$impl = $provider->getFactor();
$impl->renderValidateFactorForm(
$factor,
$form,
$viewer,
$result);
$answered_challenge = $result->getAnsweredChallenge();
if ($answered_challenge) {
$answered[] = $answered_challenge;
}
}
$form->appendRemarkupInstructions('');
if ($answered) {
$http_params = PhabricatorAuthChallenge::newHTTPParametersFromChallenges(
$answered);
foreach ($http_params as $key => $value) {
$form->addHiddenInput($key, $value);
}
}
return $form;
}
/**
* Strip the high security flag from a session.
*
* Kicks a session out of high security and logs the exit.
*
* @param PhabricatorUser Acting user.
* @param PhabricatorAuthSession Session to return to normal security.
* @return void
* @task hisec
*/
public function exitHighSecurity(
PhabricatorUser $viewer,
PhabricatorAuthSession $session) {
if (!$session->getHighSecurityUntil()) {
return;
}
queryfx(
$session->establishConnection('w'),
'UPDATE %T SET highSecurityUntil = NULL WHERE id = %d',
$session->getTableName(),
$session->getID());
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$viewer->getPHID(),
PhabricatorUserLog::ACTION_EXIT_HISEC);
$log->save();
}
/* -( Partial Sessions )--------------------------------------------------- */
/**
* Upgrade a partial session to a full session.
*
* @param PhabricatorAuthSession Session to upgrade.
* @return void
* @task partial
*/
public function upgradePartialSession(PhabricatorUser $viewer) {
if (!$viewer->hasSession()) {
throw new Exception(
pht('Upgrading partial session of user with no session!'));
}
$session = $viewer->getSession();
if (!$session->getIsPartial()) {
throw new Exception(pht('Session is not partial!'));
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$session->setIsPartial(0);
queryfx(
$session->establishConnection('w'),
'UPDATE %T SET isPartial = %d WHERE id = %d',
$session->getTableName(),
0,
$session->getID());
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$viewer->getPHID(),
PhabricatorUserLog::ACTION_LOGIN_FULL);
$log->save();
unset($unguarded);
}
/* -( Legalpad Documents )-------------------------------------------------- */
/**
* Upgrade a session to have all legalpad documents signed.
*
* @param PhabricatorUser User whose session should upgrade.
* @param array LegalpadDocument objects
* @return void
* @task partial
*/
public function signLegalpadDocuments(PhabricatorUser $viewer, array $docs) {
if (!$viewer->hasSession()) {
throw new Exception(
pht('Signing session legalpad documents of user with no session!'));
}
$session = $viewer->getSession();
if ($session->getSignedLegalpadDocuments()) {
throw new Exception(pht(
'Session has already signed required legalpad documents!'));
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$session->setSignedLegalpadDocuments(1);
queryfx(
$session->establishConnection('w'),
'UPDATE %T SET signedLegalpadDocuments = %d WHERE id = %d',
$session->getTableName(),
1,
$session->getID());
if (!empty($docs)) {
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$viewer->getPHID(),
PhabricatorUserLog::ACTION_LOGIN_LEGALPAD);
$log->save();
}
unset($unguarded);
}
/* -( One Time Login URIs )------------------------------------------------ */
/**
* Retrieve a temporary, one-time URI which can log in to an account.
*
* These URIs are used for password recovery and to regain access to accounts
* which users have been locked out of.
*
* @param PhabricatorUser User to generate a URI for.
* @param PhabricatorUserEmail Optionally, email to verify when
* link is used.
* @param string Optional context string for the URI. This is purely cosmetic
* and used only to customize workflow and error messages.
* @param bool True to generate a URI which forces an immediate upgrade to
* a full session, bypassing MFA and other login checks.
* @return string Login URI.
* @task onetime
*/
public function getOneTimeLoginURI(
PhabricatorUser $user,
PhabricatorUserEmail $email = null,
$type = self::ONETIME_RESET,
$force_full_session = false) {
$key = Filesystem::readRandomCharacters(32);
$key_hash = $this->getOneTimeLoginKeyHash($user, $email, $key);
$onetime_type = PhabricatorAuthOneTimeLoginTemporaryTokenType::TOKENTYPE;
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$token = id(new PhabricatorAuthTemporaryToken())
->setTokenResource($user->getPHID())
->setTokenType($onetime_type)
->setTokenExpires(time() + phutil_units('1 day in seconds'))
->setTokenCode($key_hash)
->setShouldForceFullSession($force_full_session)
->save();
unset($unguarded);
$uri = '/login/once/'.$type.'/'.$user->getID().'/'.$key.'/';
if ($email) {
$uri = $uri.$email->getID().'/';
}
try {
$uri = PhabricatorEnv::getProductionURI($uri);
} catch (Exception $ex) {
// If a user runs `bin/auth recover` before configuring the base URI,
// just show the path. We don't have any way to figure out the domain.
// See T4132.
}
return $uri;
}
/**
* Load the temporary token associated with a given one-time login key.
*
* @param PhabricatorUser User to load the token for.
* @param PhabricatorUserEmail Optionally, email to verify when
* link is used.
* @param string Key user is presenting as a valid one-time login key.
* @return PhabricatorAuthTemporaryToken|null Token, if one exists.
* @task onetime
*/
public function loadOneTimeLoginKey(
PhabricatorUser $user,
PhabricatorUserEmail $email = null,
$key = null) {
$key_hash = $this->getOneTimeLoginKeyHash($user, $email, $key);
$onetime_type = PhabricatorAuthOneTimeLoginTemporaryTokenType::TOKENTYPE;
return id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer($user)
->withTokenResources(array($user->getPHID()))
->withTokenTypes(array($onetime_type))
->withTokenCodes(array($key_hash))
->withExpired(false)
->executeOne();
}
/**
* Hash a one-time login key for storage as a temporary token.
*
* @param PhabricatorUser User this key is for.
* @param PhabricatorUserEmail Optionally, email to verify when
* link is used.
* @param string The one time login key.
* @return string Hash of the key.
* task onetime
*/
private function getOneTimeLoginKeyHash(
PhabricatorUser $user,
PhabricatorUserEmail $email = null,
$key = null) {
$parts = array(
$key,
$user->getAccountSecret(),
);
if ($email) {
$parts[] = $email->getVerificationCode();
}
return PhabricatorHash::weakDigest(implode(':', $parts));
}
/* -( User Cache )--------------------------------------------------------- */
/**
* @task cache
*/
private function getUserCacheQueryParts(AphrontDatabaseConnection $conn) {
$cache_selects = array();
$cache_joins = array();
$cache_map = array();
$keys = array();
$types_map = array();
$cache_types = PhabricatorUserCacheType::getAllCacheTypes();
foreach ($cache_types as $cache_type) {
foreach ($cache_type->getAutoloadKeys() as $autoload_key) {
$keys[] = $autoload_key;
$types_map[$autoload_key] = $cache_type;
}
}
$cache_table = id(new PhabricatorUserCache())->getTableName();
$cache_idx = 1;
foreach ($keys as $key) {
$join_as = 'ucache_'.$cache_idx;
$select_as = 'ucache_'.$cache_idx.'_v';
$cache_selects[] = qsprintf(
$conn,
'%T.cacheData %T',
$join_as,
$select_as);
$cache_joins[] = qsprintf(
$conn,
'LEFT JOIN %T AS %T ON u.phid = %T.userPHID
AND %T.cacheIndex = %s',
$cache_table,
$join_as,
$join_as,
$join_as,
PhabricatorHash::digestForIndex($key));
$cache_map[$select_as] = $key;
$cache_idx++;
}
if ($cache_selects) {
$cache_selects = qsprintf($conn, ', %LQ', $cache_selects);
} else {
$cache_selects = qsprintf($conn, '');
}
if ($cache_joins) {
$cache_joins = qsprintf($conn, '%LJ', $cache_joins);
} else {
$cache_joins = qsprintf($conn, '');
}
return array($cache_selects, $cache_joins, $cache_map, $types_map);
}
private function filterRawCacheData(
PhabricatorUser $user,
array $types_map,
array $cache_raw) {
foreach ($cache_raw as $cache_key => $cache_data) {
$type = $types_map[$cache_key];
if ($type->shouldValidateRawCacheData()) {
if (!$type->isRawCacheDataValid($user, $cache_key, $cache_data)) {
unset($cache_raw[$cache_key]);
}
}
}
return $cache_raw;
}
public function willServeRequestForUser(PhabricatorUser $user) {
// We allow the login user to generate any missing cache data inline.
$user->setAllowInlineCacheGeneration(true);
// Switch to the user's translation.
PhabricatorEnv::setLocaleCode($user->getTranslation());
$extensions = PhabricatorAuthSessionEngineExtension::getAllExtensions();
foreach ($extensions as $extension) {
$extension->willServeRequestForUser($user);
}
}
private function extendSession(PhabricatorAuthSession $session) {
$is_partial = $session->getIsPartial();
// Don't extend partial sessions. You have a relatively short window to
// upgrade into a full session, and your session expires otherwise.
if ($is_partial) {
return;
}
$session_type = $session->getType();
$ttl = PhabricatorAuthSession::getSessionTypeTTL(
$session_type,
$session->getIsPartial());
// If more than 20% of the time on this session has been used, refresh the
// TTL back up to the full duration. The idea here is that sessions are
// good forever if used regularly, but get GC'd when they fall out of use.
$now = PhabricatorTime::getNow();
if ($now + (0.80 * $ttl) <= $session->getSessionExpires()) {
return;
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$session->establishConnection('w'),
'UPDATE %R SET sessionExpires = UNIX_TIMESTAMP() + %d
WHERE id = %d',
$session,
$ttl,
$session->getID());
unset($unguarded);
}
}
diff --git a/src/applications/auth/extension/PhabricatorAuthMainMenuBarExtension.php b/src/applications/auth/extension/PhabricatorAuthMainMenuBarExtension.php
index d9fb5d013..d49a447df 100644
--- a/src/applications/auth/extension/PhabricatorAuthMainMenuBarExtension.php
+++ b/src/applications/auth/extension/PhabricatorAuthMainMenuBarExtension.php
@@ -1,56 +1,56 @@
<?php
final class PhabricatorAuthMainMenuBarExtension
extends PhabricatorMainMenuBarExtension {
const MAINMENUBARKEY = 'auth';
public function isExtensionEnabledForViewer(PhabricatorUser $viewer) {
return true;
}
public function shouldRequireFullSession() {
return false;
}
public function getExtensionOrder() {
return 900;
}
public function buildMainMenus() {
$viewer = $this->getViewer();
if ($viewer->isLoggedIn()) {
return array();
}
$controller = $this->getController();
if ($controller instanceof PhabricatorAuthController) {
// Don't show the "Login" item on auth controllers, since they're
// generally all related to logging in anyway.
return array();
}
return array(
$this->buildLoginMenu(),
);
}
private function buildLoginMenu() {
$controller = $this->getController();
$uri = new PhutilURI('/auth/start/');
if ($controller) {
$path = $controller->getRequest()->getPath();
- $uri->setQueryParam('next', $path);
+ $uri->replaceQueryParam('next', $path);
}
return id(new PHUIButtonView())
->setTag('a')
->setText(pht('Log In'))
->setHref($uri)
->setNoCSS(true)
->addClass('phabricator-core-login-button');
}
}
diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php
index ec49f7f74..fefd9b5fd 100644
--- a/src/applications/auth/factor/PhabricatorAuthFactor.php
+++ b/src/applications/auth/factor/PhabricatorAuthFactor.php
@@ -1,566 +1,626 @@
<?php
abstract class PhabricatorAuthFactor extends Phobject {
abstract public function getFactorName();
abstract public function getFactorShortName();
abstract public function getFactorKey();
abstract public function getFactorCreateHelp();
abstract public function getFactorDescription();
abstract public function processAddFactorForm(
PhabricatorAuthFactorProvider $provider,
AphrontFormView $form,
AphrontRequest $request,
PhabricatorUser $user);
abstract public function renderValidateFactorForm(
PhabricatorAuthFactorConfig $config,
AphrontFormView $form,
PhabricatorUser $viewer,
PhabricatorAuthFactorResult $validation_result);
public function getParameterName(
PhabricatorAuthFactorConfig $config,
$name) {
return 'authfactor.'.$config->getID().'.'.$name;
}
public static function getAllFactors() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getFactorKey')
->execute();
}
protected function newConfigForUser(PhabricatorUser $user) {
return id(new PhabricatorAuthFactorConfig())
->setUserPHID($user->getPHID())
->setFactorSecret('');
}
protected function newResult() {
return new PhabricatorAuthFactorResult();
}
public function newIconView() {
return id(new PHUIIconView())
->setIcon('fa-mobile');
}
public function canCreateNewProvider() {
return true;
}
public function getProviderCreateDescription() {
return null;
}
public function canCreateNewConfiguration(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
return true;
}
public function getConfigurationCreateDescription(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
return null;
}
public function getConfigurationListDetails(
PhabricatorAuthFactorConfig $config,
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $viewer) {
return null;
}
public function newEditEngineFields(
PhabricatorEditEngine $engine,
PhabricatorAuthFactorProvider $provider) {
return array();
}
+ public function newChallengeStatusView(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorAuthFactorProvider $provider,
+ PhabricatorUser $viewer,
+ PhabricatorAuthChallenge $challenge) {
+ return null;
+ }
+
/**
* Is this a factor which depends on the user's contact number?
*
* If a user has a "contact number" factor configured, they can not modify
* or switch their primary contact number.
*
* @return bool True if this factor should lock contact numbers.
*/
public function isContactNumberFactor() {
return false;
}
abstract public function getEnrollDescription(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user);
public function getEnrollButtonText(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
return pht('Continue');
}
public function getFactorOrder() {
return 1000;
}
final public function newSortVector() {
return id(new PhutilSortVector())
->addInt($this->canCreateNewProvider() ? 0 : 1)
->addInt($this->getFactorOrder())
->addString($this->getFactorName());
}
protected function newChallenge(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer) {
$engine = $config->getSessionEngine();
return PhabricatorAuthChallenge::initializeNewChallenge()
->setUserPHID($viewer->getPHID())
->setSessionPHID($viewer->getSession()->getPHID())
->setFactorPHID($config->getPHID())
+ ->setIsNewChallenge(true)
->setWorkflowKey($engine->getWorkflowKey());
}
abstract public function getRequestHasChallengeResponse(
PhabricatorAuthFactorConfig $config,
AphrontRequest $response);
final public function getNewIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges) {
assert_instances_of($challenges, 'PhabricatorAuthChallenge');
$now = PhabricatorTime::getNow();
// Factor implementations may need to perform writes in order to issue
// challenges, particularly push factors like SMS.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$new_challenges = $this->newIssuedChallenges(
$config,
$viewer,
$challenges);
if ($this->isAuthResult($new_challenges)) {
unset($unguarded);
return $new_challenges;
}
assert_instances_of($new_challenges, 'PhabricatorAuthChallenge');
foreach ($new_challenges as $new_challenge) {
$ttl = $new_challenge->getChallengeTTL();
if (!$ttl) {
throw new Exception(
pht('Newly issued MFA challenges must have a valid TTL!'));
}
if ($ttl < $now) {
throw new Exception(
pht(
'Newly issued MFA challenges must have a future TTL. This '.
'factor issued a bad TTL ("%s"). (Did you use a relative '.
'time instead of an epoch?)',
$ttl));
}
}
foreach ($new_challenges as $challenge) {
$challenge->save();
}
unset($unguarded);
return $new_challenges;
}
abstract protected function newIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges);
final public function getResultFromIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges) {
assert_instances_of($challenges, 'PhabricatorAuthChallenge');
$result = $this->newResultFromIssuedChallenges(
$config,
$viewer,
$challenges);
if ($result === null) {
return $result;
}
if (!$this->isAuthResult($result)) {
throw new Exception(
pht(
'Expected "newResultFromIssuedChallenges()" to return null or '.
'an object of class "%s"; got something else (in "%s").',
'PhabricatorAuthFactorResult',
get_class($this)));
}
- $result->setIssuedChallenges($challenges);
+ return $result;
+ }
+
+ final public function getResultForPrompt(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ AphrontRequest $request,
+ array $challenges) {
+ assert_instances_of($challenges, 'PhabricatorAuthChallenge');
+
+ $result = $this->newResultForPrompt(
+ $config,
+ $viewer,
+ $request,
+ $challenges);
+
+ if (!$this->isAuthResult($result)) {
+ throw new Exception(
+ pht(
+ 'Expected "newResultForPrompt()" to return an object of class "%s", '.
+ 'but it returned something else ("%s"; in "%s").',
+ 'PhabricatorAuthFactorResult',
+ phutil_describe_type($result),
+ get_class($this)));
+ }
return $result;
}
+ protected function newResultForPrompt(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ AphrontRequest $request,
+ array $challenges) {
+ return $this->newResult();
+ }
+
abstract protected function newResultFromIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges);
final public function getResultFromChallengeResponse(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
AphrontRequest $request,
array $challenges) {
assert_instances_of($challenges, 'PhabricatorAuthChallenge');
$result = $this->newResultFromChallengeResponse(
$config,
$viewer,
$request,
$challenges);
if (!$this->isAuthResult($result)) {
throw new Exception(
pht(
'Expected "newResultFromChallengeResponse()" to return an object '.
'of class "%s"; got something else (in "%s").',
'PhabricatorAuthFactorResult',
get_class($this)));
}
- $result->setIssuedChallenges($challenges);
-
return $result;
}
abstract protected function newResultFromChallengeResponse(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
AphrontRequest $request,
array $challenges);
final protected function newAutomaticControl(
PhabricatorAuthFactorResult $result) {
$is_error = $result->getIsError();
if ($is_error) {
return $this->newErrorControl($result);
}
$is_continue = $result->getIsContinue();
if ($is_continue) {
return $this->newContinueControl($result);
}
$is_answered = (bool)$result->getAnsweredChallenge();
if ($is_answered) {
return $this->newAnsweredControl($result);
}
$is_wait = $result->getIsWait();
if ($is_wait) {
return $this->newWaitControl($result);
}
return null;
}
private function newWaitControl(
PhabricatorAuthFactorResult $result) {
$error = $result->getErrorMessage();
- $icon = id(new PHUIIconView())
- ->setIcon('fa-clock-o', 'red');
+ $icon = $result->getIcon();
+ if (!$icon) {
+ $icon = id(new PHUIIconView())
+ ->setIcon('fa-clock-o', 'red');
+ }
return id(new PHUIFormTimerControl())
->setIcon($icon)
->appendChild($error)
->setError(pht('Wait'));
}
private function newAnsweredControl(
PhabricatorAuthFactorResult $result) {
- $icon = id(new PHUIIconView())
- ->setIcon('fa-check-circle-o', 'green');
+ $icon = $result->getIcon();
+ if (!$icon) {
+ $icon = id(new PHUIIconView())
+ ->setIcon('fa-check-circle-o', 'green');
+ }
return id(new PHUIFormTimerControl())
->setIcon($icon)
->appendChild(
pht('You responded to this challenge correctly.'));
}
private function newErrorControl(
PhabricatorAuthFactorResult $result) {
$error = $result->getErrorMessage();
- $icon = id(new PHUIIconView())
- ->setIcon('fa-times', 'red');
+ $icon = $result->getIcon();
+ if (!$icon) {
+ $icon = id(new PHUIIconView())
+ ->setIcon('fa-times', 'red');
+ }
return id(new PHUIFormTimerControl())
->setIcon($icon)
->appendChild($error)
->setError(pht('Error'));
}
private function newContinueControl(
PhabricatorAuthFactorResult $result) {
$error = $result->getErrorMessage();
- $icon = id(new PHUIIconView())
- ->setIcon('fa-commenting', 'green');
+ $icon = $result->getIcon();
+ if (!$icon) {
+ $icon = id(new PHUIIconView())
+ ->setIcon('fa-commenting', 'green');
+ }
- return id(new PHUIFormTimerControl())
+ $control = id(new PHUIFormTimerControl())
->setIcon($icon)
->appendChild($error);
+
+ $status_challenge = $result->getStatusChallenge();
+ if ($status_challenge) {
+ $id = $status_challenge->getID();
+ $uri = "/auth/mfa/challenge/status/{$id}/";
+ $control->setUpdateURI($uri);
+ }
+
+ return $control;
}
/* -( Synchronizing New Factors )------------------------------------------ */
final protected function loadMFASyncToken(
PhabricatorAuthFactorProvider $provider,
AphrontRequest $request,
AphrontFormView $form,
PhabricatorUser $user) {
// If the form included a synchronization key, load the corresponding
// token. The user must synchronize to a key we generated because this
// raises the barrier to theoretical attacks where an attacker might
// provide a known key for factors like TOTP.
// (We store and verify the hash of the key, not the key itself, to limit
// how useful the data in the table is to an attacker.)
$sync_type = PhabricatorAuthMFASyncTemporaryTokenType::TOKENTYPE;
$sync_token = null;
$sync_key = $request->getStr($this->getMFASyncTokenFormKey());
if (strlen($sync_key)) {
$sync_key_digest = PhabricatorHash::digestWithNamedKey(
$sync_key,
PhabricatorAuthMFASyncTemporaryTokenType::DIGEST_KEY);
$sync_token = id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer($user)
->withTokenResources(array($user->getPHID()))
->withTokenTypes(array($sync_type))
->withExpired(false)
->withTokenCodes(array($sync_key_digest))
->executeOne();
}
if (!$sync_token) {
// Don't generate a new sync token if there are too many outstanding
// tokens already. This is mostly relevant for push factors like SMS,
// where generating a token has the side effect of sending a user a
// message.
$outstanding_limit = 10;
$outstanding_tokens = id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer($user)
->withTokenResources(array($user->getPHID()))
->withTokenTypes(array($sync_type))
->withExpired(false)
->execute();
if (count($outstanding_tokens) > $outstanding_limit) {
throw new Exception(
pht(
'Your account has too many outstanding, incomplete MFA '.
'synchronization attempts. Wait an hour and try again.'));
}
$now = PhabricatorTime::getNow();
$sync_key = Filesystem::readRandomCharacters(32);
$sync_key_digest = PhabricatorHash::digestWithNamedKey(
$sync_key,
PhabricatorAuthMFASyncTemporaryTokenType::DIGEST_KEY);
$sync_ttl = $this->getMFASyncTokenTTL();
$sync_token = id(new PhabricatorAuthTemporaryToken())
->setIsNewTemporaryToken(true)
->setTokenResource($user->getPHID())
->setTokenType($sync_type)
->setTokenCode($sync_key_digest)
->setTokenExpires($now + $sync_ttl);
$properties = $this->newMFASyncTokenProperties(
$provider,
$user);
if ($this->isAuthResult($properties)) {
return $properties;
}
foreach ($properties as $key => $value) {
$sync_token->setTemporaryTokenProperty($key, $value);
}
$sync_token->save();
}
$form->addHiddenInput($this->getMFASyncTokenFormKey(), $sync_key);
return $sync_token;
}
protected function newMFASyncTokenProperties(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
return array();
}
private function getMFASyncTokenFormKey() {
return 'sync.key';
}
private function getMFASyncTokenTTL() {
return phutil_units('1 hour in seconds');
}
final protected function getChallengeForCurrentContext(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges) {
$session_phid = $viewer->getSession()->getPHID();
$engine = $config->getSessionEngine();
$workflow_key = $engine->getWorkflowKey();
foreach ($challenges as $challenge) {
if ($challenge->getSessionPHID() !== $session_phid) {
continue;
}
if ($challenge->getWorkflowKey() !== $workflow_key) {
continue;
}
if ($challenge->getIsCompleted()) {
continue;
}
if ($challenge->getIsReusedChallenge()) {
continue;
}
return $challenge;
}
return null;
}
/**
* @phutil-external-symbol class QRcode
*/
final protected function newQRCode($uri) {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/phpqrcode/phpqrcode.php';
$lines = QRcode::text($uri);
$total_width = 240;
$cell_size = floor($total_width / count($lines));
$rows = array();
foreach ($lines as $line) {
$cells = array();
for ($ii = 0; $ii < strlen($line); $ii++) {
if ($line[$ii] == '1') {
$color = '#000';
} else {
$color = '#fff';
}
$cells[] = phutil_tag(
'td',
array(
'width' => $cell_size,
'height' => $cell_size,
'style' => 'background: '.$color,
),
'');
}
$rows[] = phutil_tag('tr', array(), $cells);
}
return phutil_tag(
'table',
array(
'style' => 'margin: 24px auto;',
),
$rows);
}
final protected function getInstallDisplayName() {
$uri = PhabricatorEnv::getURI('/');
$uri = new PhutilURI($uri);
return $uri->getDomain();
}
final protected function getChallengeResponseParameterName(
PhabricatorAuthFactorConfig $config) {
return $this->getParameterName($config, 'mfa.response');
}
final protected function getChallengeResponseFromRequest(
PhabricatorAuthFactorConfig $config,
AphrontRequest $request) {
$name = $this->getChallengeResponseParameterName($config);
$value = $request->getStr($name);
$value = (string)$value;
$value = trim($value);
return $value;
}
final protected function hasCSRF(PhabricatorAuthFactorConfig $config) {
$engine = $config->getSessionEngine();
$request = $engine->getRequest();
if (!$request->isHTTPPost()) {
return false;
}
return $request->validateCSRF();
}
final protected function loadConfigurationsForProvider(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
return id(new PhabricatorAuthFactorConfigQuery())
->setViewer($user)
->withUserPHIDs(array($user->getPHID()))
->withFactorProviderPHIDs(array($provider->getPHID()))
->execute();
}
final protected function isAuthResult($object) {
return ($object instanceof PhabricatorAuthFactorResult);
}
}
diff --git a/src/applications/auth/factor/PhabricatorAuthFactorResult.php b/src/applications/auth/factor/PhabricatorAuthFactorResult.php
index 2282f162a..b5da37954 100644
--- a/src/applications/auth/factor/PhabricatorAuthFactorResult.php
+++ b/src/applications/auth/factor/PhabricatorAuthFactorResult.php
@@ -1,95 +1,105 @@
<?php
final class PhabricatorAuthFactorResult
extends Phobject {
private $answeredChallenge;
private $isWait = false;
private $isError = false;
private $isContinue = false;
private $errorMessage;
private $value;
private $issuedChallenges = array();
+ private $icon;
+ private $statusChallenge;
public function setAnsweredChallenge(PhabricatorAuthChallenge $challenge) {
if (!$challenge->getIsAnsweredChallenge()) {
throw new PhutilInvalidStateException('markChallengeAsAnswered');
}
if ($challenge->getIsCompleted()) {
throw new Exception(
pht(
'A completed challenge was provided as an answered challenge. '.
'The underlying factor is implemented improperly, challenges '.
'may not be reused.'));
}
$this->answeredChallenge = $challenge;
return $this;
}
public function getAnsweredChallenge() {
return $this->answeredChallenge;
}
+ public function setStatusChallenge(PhabricatorAuthChallenge $challenge) {
+ $this->statusChallenge = $challenge;
+ return $this;
+ }
+
+ public function getStatusChallenge() {
+ return $this->statusChallenge;
+ }
+
public function getIsValid() {
return (bool)$this->getAnsweredChallenge();
}
public function setIsWait($is_wait) {
$this->isWait = $is_wait;
return $this;
}
public function getIsWait() {
return $this->isWait;
}
public function setIsError($is_error) {
$this->isError = $is_error;
return $this;
}
public function getIsError() {
return $this->isError;
}
public function setIsContinue($is_continue) {
$this->isContinue = $is_continue;
return $this;
}
public function getIsContinue() {
return $this->isContinue;
}
public function setErrorMessage($error_message) {
$this->errorMessage = $error_message;
return $this;
}
public function getErrorMessage() {
return $this->errorMessage;
}
public function setValue($value) {
$this->value = $value;
return $this;
}
public function getValue() {
return $this->value;
}
- public function setIssuedChallenges(array $issued_challenges) {
- assert_instances_of($issued_challenges, 'PhabricatorAuthChallenge');
- $this->issuedChallenges = $issued_challenges;
+ public function setIcon(PHUIIconView $icon) {
+ $this->icon = $icon;
return $this;
}
- public function getIssuedChallenges() {
- return $this->issuedChallenges;
+ public function getIcon() {
+ return $this->icon;
}
}
diff --git a/src/applications/auth/factor/PhabricatorDuoAuthFactor.php b/src/applications/auth/factor/PhabricatorDuoAuthFactor.php
index 187e01195..a84337a76 100644
--- a/src/applications/auth/factor/PhabricatorDuoAuthFactor.php
+++ b/src/applications/auth/factor/PhabricatorDuoAuthFactor.php
@@ -1,813 +1,867 @@
<?php
final class PhabricatorDuoAuthFactor
extends PhabricatorAuthFactor {
const PROP_CREDENTIAL = 'duo.credentialPHID';
const PROP_ENROLL = 'duo.enroll';
const PROP_USERNAMES = 'duo.usernames';
const PROP_HOSTNAME = 'duo.hostname';
public function getFactorKey() {
return 'duo';
}
public function getFactorName() {
return pht('Duo Security');
}
public function getFactorShortName() {
return pht('Duo');
}
public function getFactorCreateHelp() {
return pht('Support for Duo push authentication.');
}
public function getFactorDescription() {
return pht(
'When you need to authenticate, a request will be pushed to the '.
'Duo application on your phone.');
}
public function getEnrollDescription(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
return pht(
'To add a Duo factor, first download and install the Duo application '.
'on your phone. Once you have launched the application and are ready '.
'to perform setup, click continue.');
}
public function canCreateNewConfiguration(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
if ($this->loadConfigurationsForProvider($provider, $user)) {
return false;
}
return true;
}
public function getConfigurationCreateDescription(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
$messages = array();
if ($this->loadConfigurationsForProvider($provider, $user)) {
$messages[] = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(
array(
pht(
'You already have Duo authentication attached to your account '.
'for this provider.'),
));
}
return $messages;
}
public function getConfigurationListDetails(
PhabricatorAuthFactorConfig $config,
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $viewer) {
$duo_user = $config->getAuthFactorConfigProperty('duo.username');
return pht('Duo Username: %s', $duo_user);
}
public function newEditEngineFields(
PhabricatorEditEngine $engine,
PhabricatorAuthFactorProvider $provider) {
$viewer = $engine->getViewer();
$credential_phid = $provider->getAuthFactorProviderProperty(
self::PROP_CREDENTIAL);
$hostname = $provider->getAuthFactorProviderProperty(self::PROP_HOSTNAME);
$usernames = $provider->getAuthFactorProviderProperty(self::PROP_USERNAMES);
$enroll = $provider->getAuthFactorProviderProperty(self::PROP_ENROLL);
$credential_type = PassphrasePasswordCredentialType::CREDENTIAL_TYPE;
$provides_type = PassphrasePasswordCredentialType::PROVIDES_TYPE;
$credentials = id(new PassphraseCredentialQuery())
->setViewer($viewer)
->withIsDestroyed(false)
->withProvidesTypes(array($provides_type))
->execute();
$xaction_hostname =
PhabricatorAuthFactorProviderDuoHostnameTransaction::TRANSACTIONTYPE;
$xaction_credential =
PhabricatorAuthFactorProviderDuoCredentialTransaction::TRANSACTIONTYPE;
$xaction_usernames =
PhabricatorAuthFactorProviderDuoUsernamesTransaction::TRANSACTIONTYPE;
$xaction_enroll =
PhabricatorAuthFactorProviderDuoEnrollTransaction::TRANSACTIONTYPE;
return array(
id(new PhabricatorTextEditField())
->setLabel(pht('Duo API Hostname'))
->setKey('duo.hostname')
->setValue($hostname)
->setTransactionType($xaction_hostname)
->setIsRequired(true),
id(new PhabricatorCredentialEditField())
->setLabel(pht('Duo API Credential'))
->setKey('duo.credential')
->setValue($credential_phid)
->setTransactionType($xaction_credential)
->setCredentialType($credential_type)
->setCredentials($credentials),
id(new PhabricatorSelectEditField())
->setLabel(pht('Duo Username'))
->setKey('duo.usernames')
->setValue($usernames)
->setTransactionType($xaction_usernames)
->setOptions(
array(
'username' => pht('Use Phabricator Username'),
'email' => pht('Use Primary Email Address'),
)),
id(new PhabricatorSelectEditField())
->setLabel(pht('Create Accounts'))
->setKey('duo.enroll')
->setValue($enroll)
->setTransactionType($xaction_enroll)
->setOptions(
array(
'deny' => pht('Require Existing Duo Account'),
'allow' => pht('Create New Duo Account'),
)),
);
}
public function processAddFactorForm(
PhabricatorAuthFactorProvider $provider,
AphrontFormView $form,
AphrontRequest $request,
PhabricatorUser $user) {
$token = $this->loadMFASyncToken($provider, $request, $form, $user);
if ($this->isAuthResult($token)) {
$form->appendChild($this->newAutomaticControl($token));
return;
}
$enroll = $token->getTemporaryTokenProperty('duo.enroll');
$duo_id = $token->getTemporaryTokenProperty('duo.user-id');
$duo_uri = $token->getTemporaryTokenProperty('duo.uri');
$duo_user = $token->getTemporaryTokenProperty('duo.username');
$is_external = ($enroll === 'external');
$is_auto = ($enroll === 'auto');
$is_blocked = ($enroll === 'blocked');
if (!$token->getIsNewTemporaryToken()) {
if ($is_auto) {
return $this->newDuoConfig($user, $duo_user);
} else if ($is_external || $is_blocked) {
$parameters = array(
'username' => $duo_user,
);
$result = $this->newDuoFuture($provider)
->setMethod('preauth', $parameters)
->resolve();
$result_code = $result['response']['result'];
switch ($result_code) {
case 'auth':
case 'allow':
return $this->newDuoConfig($user, $duo_user);
case 'enroll':
if ($is_blocked) {
// We'll render an equivalent static control below, so skip
// rendering here. We explicitly don't want to give the user
// an enroll workflow.
break;
}
$duo_uri = $result['response']['enroll_portal_url'];
$waiting_icon = id(new PHUIIconView())
->setIcon('fa-mobile', 'red');
$waiting_control = id(new PHUIFormTimerControl())
->setIcon($waiting_icon)
->setError(pht('Not Complete'))
->appendChild(
pht(
'You have not completed Duo enrollment yet. '.
'Complete enrollment, then click continue.'));
$form->appendControl($waiting_control);
break;
default:
case 'deny':
break;
}
} else {
$parameters = array(
'user_id' => $duo_id,
'activation_code' => $duo_uri,
);
$future = $this->newDuoFuture($provider)
->setMethod('enroll_status', $parameters);
$result = $future->resolve();
$response = $result['response'];
switch ($response) {
case 'success':
return $this->newDuoConfig($user, $duo_user);
case 'waiting':
$waiting_icon = id(new PHUIIconView())
->setIcon('fa-mobile', 'red');
$waiting_control = id(new PHUIFormTimerControl())
->setIcon($waiting_icon)
->setError(pht('Not Complete'))
->appendChild(
pht(
'You have not activated this enrollment in the Duo '.
'application on your phone yet. Complete activation, then '.
'click continue.'));
$form->appendControl($waiting_control);
break;
case 'invalid':
default:
throw new Exception(
pht(
'This Duo enrollment attempt is invalid or has '.
'expired ("%s"). Cancel the workflow and try again.',
$response));
}
}
}
if ($is_blocked) {
$blocked_icon = id(new PHUIIconView())
->setIcon('fa-times', 'red');
$blocked_control = id(new PHUIFormTimerControl())
->setIcon($blocked_icon)
->appendChild(
pht(
'Your Duo account ("%s") has not completed Duo enrollment. '.
'Check your email and complete enrollment to continue.',
phutil_tag('strong', array(), $duo_user)));
$form->appendControl($blocked_control);
} else if ($is_auto) {
$auto_icon = id(new PHUIIconView())
->setIcon('fa-check', 'green');
$auto_control = id(new PHUIFormTimerControl())
->setIcon($auto_icon)
->appendChild(
pht(
'Duo account ("%s") is fully enrolled.',
phutil_tag('strong', array(), $duo_user)));
$form->appendControl($auto_control);
} else {
$duo_button = phutil_tag(
'a',
array(
'href' => $duo_uri,
'class' => 'button button-grey',
'target' => ($is_external ? '_blank' : null),
),
pht('Enroll Duo Account: %s', $duo_user));
$duo_button = phutil_tag(
'div',
array(
'class' => 'mfa-form-enroll-button',
),
$duo_button);
if ($is_external) {
$form->appendRemarkupInstructions(
pht(
'Complete enrolling your phone with Duo:'));
$form->appendControl(
id(new AphrontFormMarkupControl())
->setValue($duo_button));
} else {
$form->appendRemarkupInstructions(
pht(
'Scan this QR code with the Duo application on your mobile '.
'phone:'));
$qr_code = $this->newQRCode($duo_uri);
$form->appendChild($qr_code);
$form->appendRemarkupInstructions(
pht(
'If you are currently using your phone to view this page, '.
'click this button to open the Duo application:'));
$form->appendControl(
id(new AphrontFormMarkupControl())
->setValue($duo_button));
}
$form->appendRemarkupInstructions(
pht(
'Once you have completed setup on your phone, click continue.'));
}
}
protected function newMFASyncTokenProperties(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
$duo_user = $this->getDuoUsername($provider, $user);
// Duo automatically normalizes usernames to lowercase. Just do that here
// so that our value agrees more closely with Duo.
$duo_user = phutil_utf8_strtolower($duo_user);
$parameters = array(
'username' => $duo_user,
);
$result = $this->newDuoFuture($provider)
->setMethod('preauth', $parameters)
->resolve();
$external_uri = null;
$result_code = $result['response']['result'];
$status_message = $result['response']['status_msg'];
switch ($result_code) {
case 'auth':
case 'allow':
// If the user already has a Duo account, they don't need to do
// anything.
return array(
'duo.enroll' => 'auto',
'duo.username' => $duo_user,
);
case 'enroll':
if (!$this->shouldAllowDuoEnrollment($provider)) {
return array(
'duo.enroll' => 'blocked',
'duo.username' => $duo_user,
);
}
$external_uri = $result['response']['enroll_portal_url'];
// Otherwise, enrollment is permitted so we're going to continue.
break;
default:
case 'deny':
return $this->newResult()
->setIsError(true)
->setErrorMessage(
pht(
'Your Duo account ("%s") is not permitted to access this '.
'system. Contact your Duo administrator for help. '.
'The Duo preauth API responded with status message ("%s"): %s',
$duo_user,
$result_code,
$status_message));
}
// Duo's "/enroll" API isn't repeatable for the same username. If we're
// the first call, great: we can do inline enrollment, which is way more
// user friendly. Otherwise, we have to send the user on an adventure.
$parameters = array(
'username' => $duo_user,
'valid_secs' => phutil_units('1 hour in seconds'),
);
try {
$result = $this->newDuoFuture($provider)
->setMethod('enroll', $parameters)
->resolve();
} catch (HTTPFutureHTTPResponseStatus $ex) {
return array(
'duo.enroll' => 'external',
'duo.username' => $duo_user,
'duo.uri' => $external_uri,
);
}
return array(
'duo.enroll' => 'inline',
'duo.uri' => $result['response']['activation_code'],
'duo.username' => $duo_user,
'duo.user-id' => $result['response']['user_id'],
);
}
protected function newIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges) {
// If we already issued a valid challenge for this workflow and session,
// don't issue a new one.
$challenge = $this->getChallengeForCurrentContext(
$config,
$viewer,
$challenges);
if ($challenge) {
return array();
}
if (!$this->hasCSRF($config)) {
return $this->newResult()
->setIsContinue(true)
->setErrorMessage(
pht(
'An authorization request will be pushed to the Duo '.
'application on your phone.'));
}
$provider = $config->getFactorProvider();
// Otherwise, issue a new challenge.
$duo_user = (string)$config->getAuthFactorConfigProperty('duo.username');
$parameters = array(
'username' => $duo_user,
);
$response = $this->newDuoFuture($provider)
->setMethod('preauth', $parameters)
->resolve();
$response = $response['response'];
$next_step = $response['result'];
$status_message = $response['status_msg'];
switch ($next_step) {
case 'auth':
// We're good to go.
break;
case 'allow':
// Duo is telling us to bypass MFA. For now, refuse.
return $this->newResult()
->setIsError(true)
->setErrorMessage(
pht(
'Duo is not requiring a challenge, which defeats the '.
'purpose of MFA. Duo must be configured to challenge you.'));
case 'enroll':
return $this->newResult()
->setIsError(true)
->setErrorMessage(
pht(
'Your Duo account ("%s") requires enrollment. Contact your '.
'Duo administrator for help. Duo status message: %s',
$duo_user,
$status_message));
case 'deny':
default:
return $this->newResult()
->setIsError(true)
->setErrorMessage(
pht(
'Your Duo account ("%s") is not permitted to access this '.
'system. Contact your Duo administrator for help. The Duo '.
'preauth API responded with status message ("%s"): %s',
$duo_user,
$next_step,
$status_message));
}
$has_push = false;
$devices = $response['devices'];
foreach ($devices as $device) {
$capabilities = array_fuse($device['capabilities']);
if (isset($capabilities['push'])) {
$has_push = true;
break;
}
}
if (!$has_push) {
return $this->newResult()
->setIsError(true)
->setErrorMessage(
pht(
'This factor has been removed from your device, so Phabricator '.
'can not send you a challenge. To continue, an administrator '.
'must strip this factor from your account.'));
}
$push_info = array(
pht('Domain') => $this->getInstallDisplayName(),
);
$push_info = phutil_build_http_querystring($push_info);
$parameters = array(
'username' => $duo_user,
'factor' => 'push',
'async' => '1',
// Duo allows us to specify a device, or to pass "auto" to have it pick
// the first one. For now, just let it pick.
'device' => 'auto',
// This is a hard-coded prefix for the word "... request" in the Duo UI,
// which defaults to "Login". We could pass richer information from
// workflows here, but it's not very flexible anyway.
'type' => 'Authentication',
'display_username' => $viewer->getUsername(),
'pushinfo' => $push_info,
);
$result = $this->newDuoFuture($provider)
->setMethod('auth', $parameters)
->resolve();
$duo_xaction = $result['response']['txid'];
// The Duo push timeout is 60 seconds. Set our challenge to expire slightly
// more quickly so that we'll re-issue a new challenge before Duo times out.
// This should keep users away from a dead-end where they can't respond to
// Duo but Phabricator won't issue a new challenge yet.
$ttl_seconds = 55;
return array(
$this->newChallenge($config, $viewer)
->setChallengeKey($duo_xaction)
->setChallengeTTL(PhabricatorTime::getNow() + $ttl_seconds),
);
}
protected function newResultFromIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges) {
$challenge = $this->getChallengeForCurrentContext(
$config,
$viewer,
$challenges);
if ($challenge->getIsAnsweredChallenge()) {
return $this->newResult()
->setAnsweredChallenge($challenge);
}
$provider = $config->getFactorProvider();
$duo_xaction = $challenge->getChallengeKey();
$parameters = array(
'txid' => $duo_xaction,
);
// This endpoint always long-polls, so use a timeout to force it to act
// more asynchronously.
try {
$result = $this->newDuoFuture($provider)
->setHTTPMethod('GET')
->setMethod('auth_status', $parameters)
- ->setTimeout(5)
+ ->setTimeout(3)
->resolve();
$state = $result['response']['result'];
$status = $result['response']['status'];
} catch (HTTPFutureCURLResponseStatus $exception) {
if ($exception->isTimeout()) {
$state = 'waiting';
$status = 'poll';
} else {
throw $exception;
}
}
$now = PhabricatorTime::getNow();
switch ($state) {
case 'allow':
$ttl = PhabricatorTime::getNow()
+ phutil_units('15 minutes in seconds');
$challenge
->markChallengeAsAnswered($ttl);
return $this->newResult()
->setAnsweredChallenge($challenge);
case 'waiting':
- // No result yet, we'll render a default state later on.
+ // If we didn't just issue this challenge, give the user a stronger
+ // hint that they need to follow the instructions.
+ if (!$challenge->getIsNewChallenge()) {
+ return $this->newResult()
+ ->setIsContinue(true)
+ ->setIcon(
+ id(new PHUIIconView())
+ ->setIcon('fa-exclamation-triangle', 'yellow'))
+ ->setErrorMessage(
+ pht(
+ 'You must approve the challenge which was sent to your '.
+ 'phone. Open the Duo application and confirm the challenge, '.
+ 'then continue.'));
+ }
+
+ // Otherwise, we'll construct a default message later on.
break;
default:
case 'deny':
if ($status === 'timeout') {
return $this->newResult()
->setIsError(true)
->setErrorMessage(
pht(
'This request has timed out because you took too long to '.
'respond.'));
} else {
$wait_duration = ($challenge->getChallengeTTL() - $now) + 1;
return $this->newResult()
->setIsWait(true)
->setErrorMessage(
pht(
'You denied this request. Wait %s second(s) to try again.',
new PhutilNumber($wait_duration)));
}
break;
}
return null;
}
public function renderValidateFactorForm(
PhabricatorAuthFactorConfig $config,
AphrontFormView $form,
PhabricatorUser $viewer,
PhabricatorAuthFactorResult $result) {
$control = $this->newAutomaticControl($result);
- if (!$control) {
- $result = $this->newResult()
- ->setIsContinue(true)
- ->setErrorMessage(
- pht(
- 'A challenge has been sent to your phone. Open the Duo '.
- 'application and confirm the challenge, then continue.'));
- $control = $this->newAutomaticControl($result);
- }
$control
->setLabel(pht('Duo'))
->setCaption(pht('Factor Name: %s', $config->getFactorName()));
$form->appendChild($control);
}
public function getRequestHasChallengeResponse(
PhabricatorAuthFactorConfig $config,
AphrontRequest $request) {
- $value = $this->getChallengeResponseFromRequest($config, $request);
- return (bool)strlen($value);
+ return false;
}
protected function newResultFromChallengeResponse(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
AphrontRequest $request,
array $challenges) {
- $challenge = $this->getChallengeForCurrentContext(
+ return $this->getResultForPrompt(
$config,
$viewer,
+ $request,
$challenges);
+ }
- $code = $this->getChallengeResponseFromRequest(
- $config,
- $request);
+ protected function newResultForPrompt(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ AphrontRequest $request,
+ array $challenges) {
$result = $this->newResult()
- ->setValue($code);
-
- if ($challenge->getIsAnsweredChallenge()) {
- return $result->setAnsweredChallenge($challenge);
- }
-
- if (phutil_hashes_are_identical($code, $challenge->getChallengeKey())) {
- $ttl = PhabricatorTime::getNow() + phutil_units('15 minutes in seconds');
-
- $challenge
- ->markChallengeAsAnswered($ttl);
-
- return $result->setAnsweredChallenge($challenge);
- }
+ ->setIsContinue(true)
+ ->setErrorMessage(
+ pht(
+ 'A challenge has been sent to your phone. Open the Duo '.
+ 'application and confirm the challenge, then continue.'));
- if (strlen($code)) {
- $error_message = pht('Invalid');
- } else {
- $error_message = pht('Required');
+ $challenge = $this->getChallengeForCurrentContext(
+ $config,
+ $viewer,
+ $challenges);
+ if ($challenge) {
+ $result
+ ->setStatusChallenge($challenge)
+ ->setIcon(
+ id(new PHUIIconView())
+ ->setIcon('fa-refresh', 'green ph-spin'));
}
- $result->setErrorMessage($error_message);
-
return $result;
}
private function newDuoFuture(PhabricatorAuthFactorProvider $provider) {
$credential_phid = $provider->getAuthFactorProviderProperty(
self::PROP_CREDENTIAL);
$omnipotent = PhabricatorUser::getOmnipotentUser();
$credential = id(new PassphraseCredentialQuery())
->setViewer($omnipotent)
->withPHIDs(array($credential_phid))
->needSecrets(true)
->executeOne();
if (!$credential) {
throw new Exception(
pht(
'Unable to load Duo API credential ("%s").',
$credential_phid));
}
$duo_key = $credential->getUsername();
$duo_secret = $credential->getSecret();
if (!$duo_secret) {
throw new Exception(
pht(
'Duo API credential ("%s") has no secret key.',
$credential_phid));
}
$duo_host = $provider->getAuthFactorProviderProperty(
self::PROP_HOSTNAME);
self::requireDuoAPIHostname($duo_host);
return id(new PhabricatorDuoFuture())
->setIntegrationKey($duo_key)
->setSecretKey($duo_secret)
->setAPIHostname($duo_host)
->setTimeout(10)
->setHTTPMethod('POST');
}
private function getDuoUsername(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
$mode = $provider->getAuthFactorProviderProperty(self::PROP_USERNAMES);
switch ($mode) {
case 'username':
return $user->getUsername();
case 'email':
return $user->loadPrimaryEmailAddress();
default:
throw new Exception(
pht(
'Duo username pairing mode ("%s") is not supported.',
$mode));
}
}
private function shouldAllowDuoEnrollment(
PhabricatorAuthFactorProvider $provider) {
$mode = $provider->getAuthFactorProviderProperty(self::PROP_ENROLL);
switch ($mode) {
case 'deny':
return false;
case 'allow':
return true;
default:
throw new Exception(
pht(
'Duo enrollment mode ("%s") is not supported.',
$mode));
}
}
private function newDuoConfig(PhabricatorUser $user, $duo_user) {
$config_properties = array(
'duo.username' => $duo_user,
);
$config = $this->newConfigForUser($user)
->setFactorName(pht('Duo (%s)', $duo_user))
->setProperties($config_properties);
return $config;
}
public static function requireDuoAPIHostname($hostname) {
if (preg_match('/\.duosecurity\.com\z/', $hostname)) {
return;
}
throw new Exception(
pht(
'Duo API hostname ("%s") is invalid, hostname must be '.
'"*.duosecurity.com".',
$hostname));
}
+ public function newChallengeStatusView(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorAuthFactorProvider $provider,
+ PhabricatorUser $viewer,
+ PhabricatorAuthChallenge $challenge) {
+
+ $duo_xaction = $challenge->getChallengeKey();
+
+ $parameters = array(
+ 'txid' => $duo_xaction,
+ );
+
+ $default_result = id(new PhabricatorAuthChallengeUpdate())
+ ->setRetry(true);
+
+ try {
+ $result = $this->newDuoFuture($provider)
+ ->setHTTPMethod('GET')
+ ->setMethod('auth_status', $parameters)
+ ->setTimeout(5)
+ ->resolve();
+
+ $state = $result['response']['result'];
+ } catch (HTTPFutureCURLResponseStatus $exception) {
+ // If we failed or timed out, retry. Usually, this is a timeout.
+ return id(new PhabricatorAuthChallengeUpdate())
+ ->setRetry(true);
+ }
+
+ // For now, don't update the view for anything but an "Allow". Updates
+ // here are just about providing more visual feedback for user convenience.
+ if ($state !== 'allow') {
+ return id(new PhabricatorAuthChallengeUpdate())
+ ->setRetry(false);
+ }
+
+ $icon = id(new PHUIIconView())
+ ->setIcon('fa-check-circle-o', 'green');
+
+ $view = id(new PHUIFormTimerControl())
+ ->setIcon($icon)
+ ->appendChild(pht('You responded to this challenge correctly.'))
+ ->newTimerView();
+
+ return id(new PhabricatorAuthChallengeUpdate())
+ ->setState('allow')
+ ->setRetry(false)
+ ->setMarkup($view);
+ }
+
}
diff --git a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php
index ba6613c01..7e77dfc11 100644
--- a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php
+++ b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php
@@ -1,452 +1,453 @@
<?php
final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
public function getFactorKey() {
return 'totp';
}
public function getFactorName() {
return pht('Mobile Phone App (TOTP)');
}
public function getFactorShortName() {
return pht('TOTP');
}
public function getFactorCreateHelp() {
return pht(
'Allow users to attach a mobile authenticator application (like '.
'Google Authenticator) to their account.');
}
public function getFactorDescription() {
return pht(
'Attach a mobile authenticator application (like Authy '.
'or Google Authenticator) to your account. When you need to '.
'authenticate, you will enter a code shown on your phone.');
}
public function getEnrollDescription(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
return pht(
'To add a TOTP factor to your account, you will first need to install '.
'a mobile authenticator application on your phone. Two applications '.
'which work well are **Google Authenticator** and **Authy**, but any '.
'other TOTP application should also work.'.
"\n\n".
'If you haven\'t already, download and install a TOTP application on '.
'your phone now. Once you\'ve launched the application and are ready '.
'to add a new TOTP code, continue to the next step.');
}
public function getConfigurationListDetails(
PhabricatorAuthFactorConfig $config,
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $viewer) {
$bits = strlen($config->getFactorSecret()) * 8;
return pht('%d-Bit Secret', $bits);
}
public function processAddFactorForm(
PhabricatorAuthFactorProvider $provider,
AphrontFormView $form,
AphrontRequest $request,
PhabricatorUser $user) {
$sync_token = $this->loadMFASyncToken(
$provider,
$request,
$form,
$user);
$secret = $sync_token->getTemporaryTokenProperty('secret');
$code = $request->getStr('totpcode');
$e_code = true;
if (!$sync_token->getIsNewTemporaryToken()) {
$okay = (bool)$this->getTimestepAtWhichResponseIsValid(
$this->getAllowedTimesteps($this->getCurrentTimestep()),
new PhutilOpaqueEnvelope($secret),
$code);
if ($okay) {
$config = $this->newConfigForUser($user)
->setFactorName(pht('Mobile App (TOTP)'))
->setFactorSecret($secret)
->setMFASyncToken($sync_token);
return $config;
} else {
if (!strlen($code)) {
$e_code = pht('Required');
} else {
$e_code = pht('Invalid');
}
}
}
$form->appendInstructions(
pht(
'Scan the QR code or manually enter the key shown below into the '.
'application.'));
$prod_uri = new PhutilURI(PhabricatorEnv::getProductionURI('/'));
$issuer = $prod_uri->getDomain();
$uri = urisprintf(
'otpauth://totp/%s:%s?secret=%s&issuer=%s',
$issuer,
$user->getUsername(),
$secret,
$issuer);
$qrcode = $this->newQRCode($uri);
$form->appendChild($qrcode);
$form->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Key'))
->setValue(phutil_tag('strong', array(), $secret)));
$form->appendInstructions(
pht(
'(If given an option, select that this key is "Time Based", not '.
'"Counter Based".)'));
$form->appendInstructions(
pht(
'After entering the key, the application should display a numeric '.
'code. Enter that code below to confirm that you have configured '.
'the authenticator correctly:'));
$form->appendChild(
id(new PHUIFormNumberControl())
->setLabel(pht('TOTP Code'))
->setName('totpcode')
->setValue($code)
+ ->setAutofocus(true)
->setError($e_code));
}
protected function newIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges) {
$current_step = $this->getCurrentTimestep();
// If we already issued a valid challenge, don't issue a new one.
if ($challenges) {
return array();
}
// Otherwise, generate a new challenge for the current timestep and compute
// the TTL.
// When computing the TTL, note that we accept codes within a certain
// window of the challenge timestep to account for clock skew and users
// needing time to enter codes.
// We don't want this challenge to expire until after all valid responses
// to it are no longer valid responses to any other challenge we might
// issue in the future. If the challenge expires too quickly, we may issue
// a new challenge which can accept the same TOTP code response.
// This means that we need to keep this challenge alive for double the
// window size: if we're currently at timestep 3, the user might respond
// with the code for timestep 5. This is valid, since timestep 5 is within
// the window for timestep 3.
// But the code for timestep 5 can be used to respond at timesteps 3, 4, 5,
// 6, and 7. To prevent any valid response to this challenge from being
// used again, we need to keep this challenge active until timestep 8.
$window_size = $this->getTimestepWindowSize();
$step_duration = $this->getTimestepDuration();
$ttl_steps = ($window_size * 2) + 1;
$ttl_seconds = ($ttl_steps * $step_duration);
return array(
$this->newChallenge($config, $viewer)
->setChallengeKey($current_step)
->setChallengeTTL(PhabricatorTime::getNow() + $ttl_seconds),
);
}
public function renderValidateFactorForm(
PhabricatorAuthFactorConfig $config,
AphrontFormView $form,
PhabricatorUser $viewer,
PhabricatorAuthFactorResult $result) {
$control = $this->newAutomaticControl($result);
if (!$control) {
$value = $result->getValue();
$error = $result->getErrorMessage();
$name = $this->getChallengeResponseParameterName($config);
$control = id(new PHUIFormNumberControl())
->setName($name)
->setDisableAutocomplete(true)
->setValue($value)
->setError($error);
}
$control
->setLabel(pht('App Code'))
->setCaption(pht('Factor Name: %s', $config->getFactorName()));
$form->appendChild($control);
}
public function getRequestHasChallengeResponse(
PhabricatorAuthFactorConfig $config,
AphrontRequest $request) {
$value = $this->getChallengeResponseFromRequest($config, $request);
return (bool)strlen($value);
}
protected function newResultFromIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges) {
// If we've already issued a challenge at the current timestep or any
// nearby timestep, require that it was issued to the current session.
// This is defusing attacks where you (broadly) look at someone's phone
// and type the code in more quickly than they do.
$session_phid = $viewer->getSession()->getPHID();
$now = PhabricatorTime::getNow();
$engine = $config->getSessionEngine();
$workflow_key = $engine->getWorkflowKey();
$current_timestep = $this->getCurrentTimestep();
foreach ($challenges as $challenge) {
$challenge_timestep = (int)$challenge->getChallengeKey();
$wait_duration = ($challenge->getChallengeTTL() - $now) + 1;
if ($challenge->getSessionPHID() !== $session_phid) {
return $this->newResult()
->setIsWait(true)
->setErrorMessage(
pht(
'This factor recently issued a challenge to a different login '.
'session. Wait %s second(s) for the code to cycle, then try '.
'again.',
new PhutilNumber($wait_duration)));
}
if ($challenge->getWorkflowKey() !== $workflow_key) {
return $this->newResult()
->setIsWait(true)
->setErrorMessage(
pht(
'This factor recently issued a challenge for a different '.
'workflow. Wait %s second(s) for the code to cycle, then try '.
'again.',
new PhutilNumber($wait_duration)));
}
// If the current realtime timestep isn't a valid response to the current
// challenge but the challenge hasn't expired yet, we're locking out
// the factor to prevent challenge windows from overlapping. Let the user
// know that they should wait for a new challenge.
$challenge_timesteps = $this->getAllowedTimesteps($challenge_timestep);
if (!isset($challenge_timesteps[$current_timestep])) {
return $this->newResult()
->setIsWait(true)
->setErrorMessage(
pht(
'This factor recently issued a challenge which has expired. '.
'A new challenge can not be issued yet. Wait %s second(s) for '.
'the code to cycle, then try again.',
new PhutilNumber($wait_duration)));
}
if ($challenge->getIsReusedChallenge()) {
return $this->newResult()
->setIsWait(true)
->setErrorMessage(
pht(
'You recently provided a response to this factor. Responses '.
'may not be reused. Wait %s second(s) for the code to cycle, '.
'then try again.',
new PhutilNumber($wait_duration)));
}
}
return null;
}
protected function newResultFromChallengeResponse(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
AphrontRequest $request,
array $challenges) {
$code = $this->getChallengeResponseFromRequest(
$config,
$request);
$result = $this->newResult()
->setValue($code);
// We expect to reach TOTP validation with exactly one valid challenge.
if (count($challenges) !== 1) {
throw new Exception(
pht(
'Reached TOTP challenge validation with an unexpected number of '.
'unexpired challenges (%d), expected exactly one.',
phutil_count($challenges)));
}
$challenge = head($challenges);
// If the client has already provided a valid answer to this challenge and
// submitted a token proving they answered it, we're all set.
if ($challenge->getIsAnsweredChallenge()) {
return $result->setAnsweredChallenge($challenge);
}
$challenge_timestep = (int)$challenge->getChallengeKey();
$current_timestep = $this->getCurrentTimestep();
$challenge_timesteps = $this->getAllowedTimesteps($challenge_timestep);
$current_timesteps = $this->getAllowedTimesteps($current_timestep);
// We require responses be both valid for the challenge and valid for the
// current timestep. A longer challenge TTL doesn't let you use older
// codes for a longer period of time.
$valid_timestep = $this->getTimestepAtWhichResponseIsValid(
array_intersect_key($challenge_timesteps, $current_timesteps),
new PhutilOpaqueEnvelope($config->getFactorSecret()),
$code);
if ($valid_timestep) {
$ttl = PhabricatorTime::getNow() + 60;
$challenge
->setProperty('totp.timestep', $valid_timestep)
->markChallengeAsAnswered($ttl);
$result->setAnsweredChallenge($challenge);
} else {
if (strlen($code)) {
$error_message = pht('Invalid');
} else {
$error_message = pht('Required');
}
$result->setErrorMessage($error_message);
}
return $result;
}
public static function generateNewTOTPKey() {
return strtoupper(Filesystem::readRandomCharacters(32));
}
public static function base32Decode($buf) {
$buf = strtoupper($buf);
$map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
$map = str_split($map);
$map = array_flip($map);
$out = '';
$len = strlen($buf);
$acc = 0;
$bits = 0;
for ($ii = 0; $ii < $len; $ii++) {
$chr = $buf[$ii];
$val = $map[$chr];
$acc = $acc << 5;
$acc = $acc + $val;
$bits += 5;
if ($bits >= 8) {
$bits = $bits - 8;
$out .= chr(($acc & (0xFF << $bits)) >> $bits);
}
}
return $out;
}
public static function getTOTPCode(PhutilOpaqueEnvelope $key, $timestamp) {
$binary_timestamp = pack('N*', 0).pack('N*', $timestamp);
$binary_key = self::base32Decode($key->openEnvelope());
$hash = hash_hmac('sha1', $binary_timestamp, $binary_key, true);
// See RFC 4226.
$offset = ord($hash[19]) & 0x0F;
$code = ((ord($hash[$offset + 0]) & 0x7F) << 24) |
((ord($hash[$offset + 1]) & 0xFF) << 16) |
((ord($hash[$offset + 2]) & 0xFF) << 8) |
((ord($hash[$offset + 3]) ) );
$code = ($code % 1000000);
$code = str_pad($code, 6, '0', STR_PAD_LEFT);
return $code;
}
private function getTimestepDuration() {
return 30;
}
private function getCurrentTimestep() {
$duration = $this->getTimestepDuration();
return (int)(PhabricatorTime::getNow() / $duration);
}
private function getAllowedTimesteps($at_timestep) {
$window = $this->getTimestepWindowSize();
$range = range($at_timestep - $window, $at_timestep + $window);
return array_fuse($range);
}
private function getTimestepWindowSize() {
// The user is allowed to provide a code from the recent past or the
// near future to account for minor clock skew between the client
// and server, and the time it takes to actually enter a code.
return 1;
}
private function getTimestepAtWhichResponseIsValid(
array $timesteps,
PhutilOpaqueEnvelope $key,
$code) {
foreach ($timesteps as $timestep) {
$expect_code = self::getTOTPCode($key, $timestep);
if (phutil_hashes_are_identical($code, $expect_code)) {
return $timestep;
}
}
return null;
}
protected function newMFASyncTokenProperties(
PhabricatorAuthFactorProvider $providerr,
PhabricatorUser $user) {
return array(
'secret' => self::generateNewTOTPKey(),
);
}
}
diff --git a/src/applications/auth/future/PhabricatorDuoFuture.php b/src/applications/auth/future/PhabricatorDuoFuture.php
index 81a5a2a2b..1e70ec2a5 100644
--- a/src/applications/auth/future/PhabricatorDuoFuture.php
+++ b/src/applications/auth/future/PhabricatorDuoFuture.php
@@ -1,151 +1,154 @@
<?php
final class PhabricatorDuoFuture
extends FutureProxy {
private $future;
private $integrationKey;
private $secretKey;
private $apiHostname;
private $httpMethod = 'POST';
private $method;
private $parameters;
private $timeout;
public function __construct() {
parent::__construct(null);
}
public function setIntegrationKey($integration_key) {
$this->integrationKey = $integration_key;
return $this;
}
public function setSecretKey(PhutilOpaqueEnvelope $key) {
$this->secretKey = $key;
return $this;
}
public function setAPIHostname($hostname) {
$this->apiHostname = $hostname;
return $this;
}
public function setMethod($method, array $parameters) {
$this->method = $method;
$this->parameters = $parameters;
return $this;
}
public function setTimeout($timeout) {
$this->timeout = $timeout;
return $this;
}
public function getTimeout() {
return $this->timeout;
}
public function setHTTPMethod($method) {
$this->httpMethod = $method;
return $this;
}
public function getHTTPMethod() {
return $this->httpMethod;
}
protected function getProxiedFuture() {
if (!$this->future) {
if ($this->integrationKey === null) {
throw new PhutilInvalidStateException('setIntegrationKey');
}
if ($this->secretKey === null) {
throw new PhutilInvalidStateException('setSecretKey');
}
if ($this->apiHostname === null) {
throw new PhutilInvalidStateException('setAPIHostname');
}
if ($this->method === null || $this->parameters === null) {
throw new PhutilInvalidStateException('setMethod');
}
$path = (string)urisprintf('/auth/v2/%s', $this->method);
$host = $this->apiHostname;
$host = phutil_utf8_strtolower($host);
- $uri = id(new PhutilURI(''))
- ->setProtocol('https')
- ->setDomain($host)
- ->setPath($path);
-
$data = $this->parameters;
$date = date('r');
$http_method = $this->getHTTPMethod();
ksort($data);
$data_parts = phutil_build_http_querystring($data);
$corpus = array(
$date,
$http_method,
$host,
$path,
$data_parts,
);
$corpus = implode("\n", $corpus);
$signature = hash_hmac(
'sha1',
$corpus,
$this->secretKey->openEnvelope());
$signature = new PhutilOpaqueEnvelope($signature);
if ($http_method === 'GET') {
- $uri->setQueryParams($data);
- $data = array();
+ $uri_data = $data;
+ $body_data = array();
+ } else {
+ $uri_data = array();
+ $body_data = $data;
}
- $future = id(new HTTPSFuture($uri, $data))
+ $uri = id(new PhutilURI('', $uri_data))
+ ->setProtocol('https')
+ ->setDomain($host)
+ ->setPath($path);
+
+ $future = id(new HTTPSFuture($uri, $body_data))
->setHTTPBasicAuthCredentials($this->integrationKey, $signature)
->setMethod($http_method)
->addHeader('Accept', 'application/json')
->addHeader('Date', $date);
$timeout = $this->getTimeout();
if ($timeout) {
$future->setTimeout($timeout);
}
$this->future = $future;
}
return $this->future;
}
protected function didReceiveResult($result) {
list($status, $body, $headers) = $result;
if ($status->isError()) {
throw $status;
}
try {
$data = phutil_json_decode($body);
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht('Expected JSON response from Duo.'),
$ex);
}
return $data;
}
}
diff --git a/src/applications/auth/guidance/PhabricatorAuthProvidersGuidanceEngineExtension.php b/src/applications/auth/guidance/PhabricatorAuthProvidersGuidanceEngineExtension.php
index ac3fe4d30..d1f67393c 100644
--- a/src/applications/auth/guidance/PhabricatorAuthProvidersGuidanceEngineExtension.php
+++ b/src/applications/auth/guidance/PhabricatorAuthProvidersGuidanceEngineExtension.php
@@ -1,88 +1,108 @@
<?php
final class PhabricatorAuthProvidersGuidanceEngineExtension
extends PhabricatorGuidanceEngineExtension {
const GUIDANCEKEY = 'core.auth.providers';
public function canGenerateGuidance(PhabricatorGuidanceContext $context) {
return ($context instanceof PhabricatorAuthProvidersGuidanceContext);
}
public function generateGuidance(PhabricatorGuidanceContext $context) {
+ $configs = id(new PhabricatorAuthProviderConfigQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withIsEnabled(true)
+ ->execute();
+
+ $allows_registration = false;
+ foreach ($configs as $config) {
+ $provider = $config->getProvider();
+ if ($provider->shouldAllowRegistration()) {
+ $allows_registration = true;
+ break;
+ }
+ }
+
+ // If no provider allows registration, we don't need provide any warnings
+ // about registration being too open.
+ if (!$allows_registration) {
+ return array();
+ }
+
$domains_key = 'auth.email-domains';
$domains_link = $this->renderConfigLink($domains_key);
$domains_value = PhabricatorEnv::getEnvConfig($domains_key);
$approval_key = 'auth.require-approval';
$approval_link = $this->renderConfigLink($approval_key);
$approval_value = PhabricatorEnv::getEnvConfig($approval_key);
$results = array();
if ($domains_value) {
$message = pht(
'Phabricator is configured with an email domain whitelist (in %s), so '.
'only users with a verified email address at one of these %s '.
'allowed domain(s) will be able to register an account: %s',
$domains_link,
phutil_count($domains_value),
phutil_tag('strong', array(), implode(', ', $domains_value)));
$results[] = $this->newGuidance('core.auth.email-domains.on')
->setMessage($message);
} else {
$message = pht(
'Anyone who can browse to this Phabricator install will be able to '.
'register an account. To add email domain restrictions, configure '.
'%s.',
$domains_link);
$results[] = $this->newGuidance('core.auth.email-domains.off')
->setMessage($message);
}
if ($approval_value) {
$message = pht(
'Administrative approvals are enabled (in %s), so all new users must '.
'have their accounts approved by an administrator.',
$approval_link);
$results[] = $this->newGuidance('core.auth.require-approval.on')
->setMessage($message);
} else {
$message = pht(
'Administrative approvals are disabled, so users who register will '.
'be able to use their accounts immediately. To enable approvals, '.
'configure %s.',
$approval_link);
$results[] = $this->newGuidance('core.auth.require-approval.off')
->setMessage($message);
}
if (!$domains_value && !$approval_value) {
$message = pht(
'You can safely ignore these warnings if the install itself has '.
'access controls (for example, it is deployed on a VPN) or if all of '.
'the configured providers have access controls (for example, they are '.
'all private LDAP or OAuth servers).');
$results[] = $this->newWarning('core.auth.warning')
->setMessage($message);
}
return $results;
}
private function renderConfigLink($key) {
return phutil_tag(
'a',
array(
'href' => '/config/edit/'.$key.'/',
'target' => '_blank',
),
$key);
}
}
diff --git a/src/applications/auth/handler/PhabricatorAuthLoginHandler.php b/src/applications/auth/handler/PhabricatorAuthLoginHandler.php
deleted file mode 100644
index eabbf9184..000000000
--- a/src/applications/auth/handler/PhabricatorAuthLoginHandler.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-abstract class PhabricatorAuthLoginHandler
- extends Phobject {
-
- private $request;
- private $delegatingController;
-
- public function getAuthLoginHeaderContent() {
- return array();
- }
-
- final public function setDelegatingController(AphrontController $controller) {
- $this->delegatingController = $controller;
- return $this;
- }
-
- final public function getDelegatingController() {
- return $this->delegatingController;
- }
-
- final public function setRequest(AphrontRequest $request) {
- $this->request = $request;
- return $this;
- }
-
- final public function getRequest() {
- return $this->request;
- }
-
- final public static function getAllHandlers() {
- return id(new PhutilClassMapQuery())
- ->setAncestorClass(__CLASS__)
- ->execute();
- }
-}
diff --git a/src/applications/auth/message/PhabricatorAuthLinkMessageType.php b/src/applications/auth/message/PhabricatorAuthLinkMessageType.php
new file mode 100644
index 000000000..17991231c
--- /dev/null
+++ b/src/applications/auth/message/PhabricatorAuthLinkMessageType.php
@@ -0,0 +1,18 @@
+<?php
+
+final class PhabricatorAuthLinkMessageType
+ extends PhabricatorAuthMessageType {
+
+ const MESSAGEKEY = 'auth.link';
+
+ public function getDisplayName() {
+ return pht('Unlinked Account Instructions');
+ }
+
+ public function getShortDescription() {
+ return pht(
+ 'Guidance shown after a user logs in with an email link and is '.
+ 'prompted to link an external account.');
+ }
+
+}
diff --git a/src/applications/auth/provider/PhabricatorAuthProvider.php b/src/applications/auth/provider/PhabricatorAuthProvider.php
index 0525edad5..eaf52552e 100644
--- a/src/applications/auth/provider/PhabricatorAuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorAuthProvider.php
@@ -1,519 +1,534 @@
<?php
abstract class PhabricatorAuthProvider extends Phobject {
private $providerConfig;
public function attachProviderConfig(PhabricatorAuthProviderConfig $config) {
$this->providerConfig = $config;
return $this;
}
public function hasProviderConfig() {
return (bool)$this->providerConfig;
}
public function getProviderConfig() {
if ($this->providerConfig === null) {
throw new PhutilInvalidStateException('attachProviderConfig');
}
return $this->providerConfig;
}
public function getConfigurationHelp() {
return null;
}
public function getDefaultProviderConfig() {
return id(new PhabricatorAuthProviderConfig())
->setProviderClass(get_class($this))
->setIsEnabled(1)
->setShouldAllowLogin(1)
->setShouldAllowRegistration(1)
->setShouldAllowLink(1)
->setShouldAllowUnlink(1);
}
public function getNameForCreate() {
return $this->getProviderName();
}
public function getDescriptionForCreate() {
return null;
}
public function getProviderKey() {
return $this->getAdapter()->getAdapterKey();
}
public function getProviderType() {
return $this->getAdapter()->getAdapterType();
}
public function getProviderDomain() {
return $this->getAdapter()->getAdapterDomain();
}
public static function getAllBaseProviders() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->execute();
}
public static function getAllProviders() {
static $providers;
if ($providers === null) {
$objects = self::getAllBaseProviders();
$configs = id(new PhabricatorAuthProviderConfigQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->execute();
$providers = array();
foreach ($configs as $config) {
if (!isset($objects[$config->getProviderClass()])) {
// This configuration is for a provider which is not installed.
continue;
}
$object = clone $objects[$config->getProviderClass()];
$object->attachProviderConfig($config);
$key = $object->getProviderKey();
if (isset($providers[$key])) {
throw new Exception(
pht(
"Two authentication providers use the same provider key ".
"('%s'). Each provider must be identified by a unique key.",
$key));
}
$providers[$key] = $object;
}
}
return $providers;
}
public static function getAllEnabledProviders() {
$providers = self::getAllProviders();
foreach ($providers as $key => $provider) {
if (!$provider->isEnabled()) {
unset($providers[$key]);
}
}
return $providers;
}
public static function getEnabledProviderByKey($provider_key) {
return idx(self::getAllEnabledProviders(), $provider_key);
}
abstract public function getProviderName();
abstract public function getAdapter();
public function isEnabled() {
return $this->getProviderConfig()->getIsEnabled();
}
public function shouldAllowLogin() {
return $this->getProviderConfig()->getShouldAllowLogin();
}
public function shouldAllowRegistration() {
if (!$this->shouldAllowLogin()) {
return false;
}
return $this->getProviderConfig()->getShouldAllowRegistration();
}
public function shouldAllowAccountLink() {
return $this->getProviderConfig()->getShouldAllowLink();
}
public function shouldAllowAccountUnlink() {
return $this->getProviderConfig()->getShouldAllowUnlink();
}
public function shouldTrustEmails() {
return $this->shouldAllowEmailTrustConfiguration() &&
$this->getProviderConfig()->getShouldTrustEmails();
}
/**
* Should we allow the adapter to be marked as "trusted". This is true for
* all adapters except those that allow the user to type in emails (see
* @{class:PhabricatorPasswordAuthProvider}).
*/
public function shouldAllowEmailTrustConfiguration() {
return true;
}
public function buildLoginForm(PhabricatorAuthStartController $controller) {
return $this->renderLoginForm($controller->getRequest(), $mode = 'start');
}
public function buildInviteForm(PhabricatorAuthStartController $controller) {
return $this->renderLoginForm($controller->getRequest(), $mode = 'invite');
}
abstract public function processLoginRequest(
PhabricatorAuthLoginController $controller);
- public function buildLinkForm(PhabricatorAuthLinkController $controller) {
+ public function buildLinkForm($controller) {
return $this->renderLoginForm($controller->getRequest(), $mode = 'link');
}
public function shouldAllowAccountRefresh() {
return true;
}
public function buildRefreshForm(
PhabricatorAuthLinkController $controller) {
return $this->renderLoginForm($controller->getRequest(), $mode = 'refresh');
}
protected function renderLoginForm(AphrontRequest $request, $mode) {
throw new PhutilMethodNotImplementedException();
}
public function createProviders() {
return array($this);
}
protected function willSaveAccount(PhabricatorExternalAccount $account) {
return;
}
public function willRegisterAccount(PhabricatorExternalAccount $account) {
return;
}
protected function loadOrCreateAccount($account_id) {
if (!strlen($account_id)) {
throw new Exception(pht('Empty account ID!'));
}
$adapter = $this->getAdapter();
$adapter_class = get_class($adapter);
if (!strlen($adapter->getAdapterType())) {
throw new Exception(
pht(
"AuthAdapter (of class '%s') has an invalid implementation: ".
"no adapter type.",
$adapter_class));
}
if (!strlen($adapter->getAdapterDomain())) {
throw new Exception(
pht(
"AuthAdapter (of class '%s') has an invalid implementation: ".
"no adapter domain.",
$adapter_class));
}
$account = id(new PhabricatorExternalAccount())->loadOneWhere(
'accountType = %s AND accountDomain = %s AND accountID = %s',
$adapter->getAdapterType(),
$adapter->getAdapterDomain(),
$account_id);
if (!$account) {
- $account = id(new PhabricatorExternalAccount())
- ->setAccountType($adapter->getAdapterType())
- ->setAccountDomain($adapter->getAdapterDomain())
+ $account = $this->newExternalAccount()
->setAccountID($account_id);
}
$account->setUsername($adapter->getAccountName());
$account->setRealName($adapter->getAccountRealName());
$account->setEmail($adapter->getAccountEmail());
$account->setAccountURI($adapter->getAccountURI());
$account->setProfileImagePHID(null);
$image_uri = $adapter->getAccountImageURI();
if ($image_uri) {
try {
$name = PhabricatorSlug::normalize($this->getProviderName());
$name = $name.'-profile.jpg';
// TODO: If the image has not changed, we do not need to make a new
// file entry for it, but there's no convenient way to do this with
// PhabricatorFile right now. The storage will get shared, so the impact
// here is negligible.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$image_file = PhabricatorFile::newFromFileDownload(
$image_uri,
array(
'name' => $name,
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
if ($image_file->isViewableImage()) {
$image_file
->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
->setCanCDN(true)
->save();
$account->setProfileImagePHID($image_file->getPHID());
} else {
$image_file->delete();
}
unset($unguarded);
} catch (Exception $ex) {
// Log this but proceed, it's not especially important that we
// be able to pull profile images.
phlog($ex);
}
}
$this->willSaveAccount($account);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$account->save();
unset($unguarded);
return $account;
}
public function getLoginURI() {
$app = PhabricatorApplication::getByClass('PhabricatorAuthApplication');
return $app->getApplicationURI('/login/'.$this->getProviderKey().'/');
}
public function getSettingsURI() {
return '/settings/panel/external/';
}
public function getStartURI() {
$app = PhabricatorApplication::getByClass('PhabricatorAuthApplication');
$uri = $app->getApplicationURI('/start/');
return $uri;
}
public function isDefaultRegistrationProvider() {
return false;
}
public function shouldRequireRegistrationPassword() {
return false;
}
- public function getDefaultExternalAccount() {
- throw new PhutilMethodNotImplementedException();
+ public function newDefaultExternalAccount() {
+ return $this->newExternalAccount();
+ }
+
+ protected function newExternalAccount() {
+ $config = $this->getProviderConfig();
+ $adapter = $this->getAdapter();
+
+ return id(new PhabricatorExternalAccount())
+ ->setAccountType($adapter->getAdapterType())
+ ->setAccountDomain($adapter->getAdapterDomain())
+ ->setProviderConfigPHID($config->getPHID());
}
public function getLoginOrder() {
return '500-'.$this->getProviderName();
}
protected function getLoginIcon() {
return 'Generic';
}
+ public function newIconView() {
+ return id(new PHUIIconView())
+ ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
+ ->setSpriteIcon($this->getLoginIcon());
+ }
+
public function isLoginFormAButton() {
return false;
}
public function renderConfigPropertyTransactionTitle(
PhabricatorAuthProviderConfigTransaction $xaction) {
return null;
}
public function readFormValuesFromProvider() {
return array();
}
public function readFormValuesFromRequest(AphrontRequest $request) {
return array();
}
public function processEditForm(
AphrontRequest $request,
array $values) {
$errors = array();
$issues = array();
return array($errors, $issues, $values);
}
public function extendEditForm(
AphrontRequest $request,
AphrontFormView $form,
array $values,
array $issues) {
return;
}
public function willRenderLinkedAccount(
PhabricatorUser $viewer,
PHUIObjectItemView $item,
PhabricatorExternalAccount $account) {
$account_view = id(new PhabricatorAuthAccountView())
->setExternalAccount($account)
->setAuthProvider($this);
$item->appendChild(
phutil_tag(
'div',
array(
'class' => 'mmr mml mst mmb',
),
$account_view));
}
/**
* Return true to use a two-step configuration (setup, configure) instead of
* the default single-step configuration. In practice, this means that
* creating a new provider instance will redirect back to the edit page
* instead of the provider list.
*
* @return bool True if this provider uses two-step configuration.
*/
public function hasSetupStep() {
return false;
}
/**
* Render a standard login/register button element.
*
* The `$attributes` parameter takes these keys:
*
* - `uri`: URI the button should take the user to when clicked.
* - `method`: Optional HTTP method the button should use, defaults to GET.
*
* @param AphrontRequest HTTP request.
* @param string Request mode string.
* @param map Additional parameters, see above.
* @return wild Log in button.
*/
protected function renderStandardLoginButton(
AphrontRequest $request,
$mode,
array $attributes = array()) {
PhutilTypeSpec::checkMap(
$attributes,
array(
'method' => 'optional string',
'uri' => 'string',
'sigil' => 'optional string',
));
$viewer = $request->getUser();
$adapter = $this->getAdapter();
if ($mode == 'link') {
$button_text = pht('Link External Account');
} else if ($mode == 'refresh') {
$button_text = pht('Refresh Account Link');
} else if ($mode == 'invite') {
$button_text = pht('Register Account');
} else if ($this->shouldAllowRegistration()) {
$button_text = pht('Log In or Register');
} else {
$button_text = pht('Log In');
}
$icon = id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
->setSpriteIcon($this->getLoginIcon());
$button = id(new PHUIButtonView())
->setSize(PHUIButtonView::BIG)
->setColor(PHUIButtonView::GREY)
->setIcon($icon)
->setText($button_text)
->setSubtext($this->getProviderName());
$uri = $attributes['uri'];
$uri = new PhutilURI($uri);
- $params = $uri->getQueryParams();
- $uri->setQueryParams(array());
+ $params = $uri->getQueryParamsAsPairList();
+ $uri->removeAllQueryParams();
$content = array($button);
- foreach ($params as $key => $value) {
+ foreach ($params as $pair) {
+ list($key, $value) = $pair;
$content[] = phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => $key,
'value' => $value,
));
}
$static_response = CelerityAPI::getStaticResourceResponse();
$static_response->addContentSecurityPolicyURI('form-action', (string)$uri);
foreach ($this->getContentSecurityPolicyFormActions() as $csp_uri) {
$static_response->addContentSecurityPolicyURI('form-action', $csp_uri);
}
return phabricator_form(
$viewer,
array(
'method' => idx($attributes, 'method', 'GET'),
'action' => (string)$uri,
'sigil' => idx($attributes, 'sigil'),
),
$content);
}
public function renderConfigurationFooter() {
return null;
}
public function getAuthCSRFCode(AphrontRequest $request) {
$phcid = $request->getCookie(PhabricatorCookies::COOKIE_CLIENTID);
if (!strlen($phcid)) {
throw new AphrontMalformedRequestException(
pht('Missing Client ID Cookie'),
pht(
'Your browser did not submit a "%s" cookie with client state '.
'information in the request. Check that cookies are enabled. '.
'If this problem persists, you may need to clear your cookies.',
PhabricatorCookies::COOKIE_CLIENTID),
true);
}
return PhabricatorHash::weakDigest($phcid);
}
protected function verifyAuthCSRFCode(AphrontRequest $request, $actual) {
$expect = $this->getAuthCSRFCode($request);
if (!strlen($actual)) {
throw new Exception(
pht(
'The authentication provider did not return a client state '.
'parameter in its response, but one was expected. If this '.
'problem persists, you may need to clear your cookies.'));
}
if (!phutil_hashes_are_identical($actual, $expect)) {
throw new Exception(
pht(
'The authentication provider did not return the correct client '.
'state parameter in its response. If this problem persists, you may '.
'need to clear your cookies.'));
}
}
public function supportsAutoLogin() {
return false;
}
public function getAutoLoginURI(AphrontRequest $request) {
throw new PhutilMethodNotImplementedException();
}
protected function getContentSecurityPolicyFormActions() {
return array();
}
}
diff --git a/src/applications/auth/provider/PhabricatorFacebookAuthProvider.php b/src/applications/auth/provider/PhabricatorFacebookAuthProvider.php
index e3e1fb43e..67840727e 100644
--- a/src/applications/auth/provider/PhabricatorFacebookAuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorFacebookAuthProvider.php
@@ -1,128 +1,125 @@
<?php
final class PhabricatorFacebookAuthProvider
extends PhabricatorOAuth2AuthProvider {
const KEY_REQUIRE_SECURE = 'oauth:facebook:require-secure';
public function getProviderName() {
return pht('Facebook');
}
protected function getProviderConfigurationHelp() {
$uri = PhabricatorEnv::getProductionURI($this->getLoginURI());
return pht(
'To configure Facebook OAuth, create a new Facebook Application here:'.
"\n\n".
'https://developers.facebook.com/apps'.
"\n\n".
'You should use these settings in your application:'.
"\n\n".
" - **Site URL**: Set this to `%s`\n".
" - **Valid OAuth redirect URIs**: You should also set this to `%s`\n".
" - **Client OAuth Login**: Set this to **OFF**.\n".
" - **Embedded browser OAuth Login**: Set this to **OFF**.\n".
"\n\n".
"Some of these settings may be in the **Advanced** tab.\n\n".
"After creating your new application, copy the **App ID** and ".
"**App Secret** to the fields above.",
(string)$uri,
(string)$uri);
}
public function getDefaultProviderConfig() {
return parent::getDefaultProviderConfig()
->setProperty(self::KEY_REQUIRE_SECURE, 1);
}
protected function newOAuthAdapter() {
$require_secure = $this->getProviderConfig()->getProperty(
self::KEY_REQUIRE_SECURE);
return id(new PhutilFacebookAuthAdapter())
->setRequireSecureBrowsing($require_secure);
}
protected function getLoginIcon() {
return 'Facebook';
}
+ protected function getContentSecurityPolicyFormActions() {
+ return array(
+ // See T13254. After login with a mobile device, Facebook may redirect
+ // to the mobile site.
+ 'https://m.facebook.com/',
+ );
+ }
+
public function readFormValuesFromProvider() {
$require_secure = $this->getProviderConfig()->getProperty(
self::KEY_REQUIRE_SECURE);
return parent::readFormValuesFromProvider() + array(
self::KEY_REQUIRE_SECURE => $require_secure,
);
}
public function readFormValuesFromRequest(AphrontRequest $request) {
return parent::readFormValuesFromRequest($request) + array(
self::KEY_REQUIRE_SECURE => $request->getBool(self::KEY_REQUIRE_SECURE),
);
}
public function extendEditForm(
AphrontRequest $request,
AphrontFormView $form,
array $values,
array $issues) {
parent::extendEditForm($request, $form, $values, $issues);
$key_require = self::KEY_REQUIRE_SECURE;
$v_require = idx($values, $key_require);
$form
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
$key_require,
$v_require,
pht(
"%s ".
"Require users to enable 'secure browsing' on Facebook in order ".
"to use Facebook to authenticate with Phabricator. This ".
"improves security by preventing an attacker from capturing ".
"an insecure Facebook session and escalating it into a ".
"Phabricator session. Enabling it is recommended.",
phutil_tag('strong', array(), pht('Require Secure Browsing:')))));
}
public function renderConfigPropertyTransactionTitle(
PhabricatorAuthProviderConfigTransaction $xaction) {
$author_phid = $xaction->getAuthorPHID();
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$key = $xaction->getMetadataValue(
PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY);
switch ($key) {
case self::KEY_REQUIRE_SECURE:
if ($new) {
return pht(
'%s turned "Require Secure Browsing" on.',
$xaction->renderHandleLink($author_phid));
} else {
return pht(
'%s turned "Require Secure Browsing" off.',
$xaction->renderHandleLink($author_phid));
}
}
return parent::renderConfigPropertyTransactionTitle($xaction);
}
- public static function getFacebookApplicationID() {
- $providers = PhabricatorAuthProvider::getAllProviders();
- $fb_provider = idx($providers, 'facebook:facebook.com');
- if (!$fb_provider) {
- return null;
- }
-
- return $fb_provider->getProviderConfig()->getProperty(
- self::PROPERTY_APP_ID);
- }
-
}
diff --git a/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php b/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php
index 44b58b85f..4a4babcc1 100644
--- a/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php
@@ -1,493 +1,494 @@
<?php
final class PhabricatorLDAPAuthProvider extends PhabricatorAuthProvider {
private $adapter;
public function getProviderName() {
return pht('LDAP');
}
public function getDescriptionForCreate() {
return pht(
'Configure a connection to an LDAP server so that users can use their '.
'LDAP credentials to log in to Phabricator.');
}
public function getDefaultProviderConfig() {
return parent::getDefaultProviderConfig()
->setProperty(self::KEY_PORT, 389)
->setProperty(self::KEY_VERSION, 3);
}
public function getAdapter() {
if (!$this->adapter) {
$conf = $this->getProviderConfig();
$realname_attributes = $conf->getProperty(self::KEY_REALNAME_ATTRIBUTES);
if (!is_array($realname_attributes)) {
$realname_attributes = array();
}
$search_attributes = $conf->getProperty(self::KEY_SEARCH_ATTRIBUTES);
$search_attributes = phutil_split_lines($search_attributes, false);
$search_attributes = array_filter($search_attributes);
$adapter = id(new PhutilLDAPAuthAdapter())
->setHostname(
$conf->getProperty(self::KEY_HOSTNAME))
->setPort(
$conf->getProperty(self::KEY_PORT))
->setBaseDistinguishedName(
$conf->getProperty(self::KEY_DISTINGUISHED_NAME))
->setSearchAttributes($search_attributes)
->setUsernameAttribute(
$conf->getProperty(self::KEY_USERNAME_ATTRIBUTE))
->setRealNameAttributes($realname_attributes)
->setLDAPVersion(
$conf->getProperty(self::KEY_VERSION))
->setLDAPReferrals(
$conf->getProperty(self::KEY_REFERRALS))
->setLDAPStartTLS(
$conf->getProperty(self::KEY_START_TLS))
->setAlwaysSearch($conf->getProperty(self::KEY_ALWAYS_SEARCH))
->setAnonymousUsername(
$conf->getProperty(self::KEY_ANONYMOUS_USERNAME))
->setAnonymousPassword(
new PhutilOpaqueEnvelope(
$conf->getProperty(self::KEY_ANONYMOUS_PASSWORD)))
->setActiveDirectoryDomain(
$conf->getProperty(self::KEY_ACTIVEDIRECTORY_DOMAIN));
$this->adapter = $adapter;
}
return $this->adapter;
}
protected function renderLoginForm(AphrontRequest $request, $mode) {
$viewer = $request->getUser();
$dialog = id(new AphrontDialogView())
->setSubmitURI($this->getLoginURI())
->setUser($viewer);
if ($mode == 'link') {
$dialog->setTitle(pht('Link LDAP Account'));
$dialog->addSubmitButton(pht('Link Accounts'));
$dialog->addCancelButton($this->getSettingsURI());
} else if ($mode == 'refresh') {
$dialog->setTitle(pht('Refresh LDAP Account'));
$dialog->addSubmitButton(pht('Refresh Account'));
$dialog->addCancelButton($this->getSettingsURI());
} else {
if ($this->shouldAllowRegistration()) {
$dialog->setTitle(pht('Log In or Register with LDAP'));
$dialog->addSubmitButton(pht('Log In or Register'));
} else {
$dialog->setTitle(pht('Log In with LDAP'));
$dialog->addSubmitButton(pht('Log In'));
}
if ($mode == 'login') {
$dialog->addCancelButton($this->getStartURI());
}
}
$v_user = $request->getStr('ldap_username');
$e_user = null;
$e_pass = null;
$errors = array();
if ($request->isHTTPPost()) {
// NOTE: This is intentionally vague so as not to disclose whether a
// given username exists.
$e_user = pht('Invalid');
$e_pass = pht('Invalid');
$errors[] = pht('Username or password are incorrect.');
}
$form = id(new PHUIFormLayoutView())
->setUser($viewer)
->setFullWidth(true)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('LDAP Username'))
->setName('ldap_username')
+ ->setAutofocus(true)
->setValue($v_user)
->setError($e_user))
->appendChild(
id(new AphrontFormPasswordControl())
->setLabel(pht('LDAP Password'))
->setName('ldap_password')
->setError($e_pass));
if ($errors) {
$errors = id(new PHUIInfoView())->setErrors($errors);
}
$dialog->appendChild($errors);
$dialog->appendChild($form);
return $dialog;
}
public function processLoginRequest(
PhabricatorAuthLoginController $controller) {
$request = $controller->getRequest();
$viewer = $request->getUser();
$response = null;
$account = null;
$username = $request->getStr('ldap_username');
$password = $request->getStr('ldap_password');
$has_password = strlen($password);
$password = new PhutilOpaqueEnvelope($password);
if (!strlen($username) || !$has_password) {
$response = $controller->buildProviderPageResponse(
$this,
$this->renderLoginForm($request, 'login'));
return array($account, $response);
}
if ($request->isFormPost()) {
try {
if (strlen($username) && $has_password) {
$adapter = $this->getAdapter();
$adapter->setLoginUsername($username);
$adapter->setLoginPassword($password);
// TODO: This calls ldap_bind() eventually, which dumps cleartext
// passwords to the error log. See note in PhutilLDAPAuthAdapter.
// See T3351.
DarkConsoleErrorLogPluginAPI::enableDiscardMode();
$account_id = $adapter->getAccountID();
DarkConsoleErrorLogPluginAPI::disableDiscardMode();
} else {
throw new Exception(pht('Username and password are required!'));
}
} catch (PhutilAuthCredentialException $ex) {
$response = $controller->buildProviderPageResponse(
$this,
$this->renderLoginForm($request, 'login'));
return array($account, $response);
} catch (Exception $ex) {
// TODO: Make this cleaner.
throw $ex;
}
}
return array($this->loadOrCreateAccount($account_id), $response);
}
const KEY_HOSTNAME = 'ldap:host';
const KEY_PORT = 'ldap:port';
const KEY_DISTINGUISHED_NAME = 'ldap:dn';
const KEY_SEARCH_ATTRIBUTES = 'ldap:search-attribute';
const KEY_USERNAME_ATTRIBUTE = 'ldap:username-attribute';
const KEY_REALNAME_ATTRIBUTES = 'ldap:realname-attributes';
const KEY_VERSION = 'ldap:version';
const KEY_REFERRALS = 'ldap:referrals';
const KEY_START_TLS = 'ldap:start-tls';
// TODO: This is misspelled! See T13005.
const KEY_ANONYMOUS_USERNAME = 'ldap:anoynmous-username';
const KEY_ANONYMOUS_PASSWORD = 'ldap:anonymous-password';
const KEY_ALWAYS_SEARCH = 'ldap:always-search';
const KEY_ACTIVEDIRECTORY_DOMAIN = 'ldap:activedirectory-domain';
private function getPropertyKeys() {
return array_keys($this->getPropertyLabels());
}
private function getPropertyLabels() {
return array(
self::KEY_HOSTNAME => pht('LDAP Hostname'),
self::KEY_PORT => pht('LDAP Port'),
self::KEY_DISTINGUISHED_NAME => pht('Base Distinguished Name'),
self::KEY_SEARCH_ATTRIBUTES => pht('Search Attributes'),
self::KEY_ALWAYS_SEARCH => pht('Always Search'),
self::KEY_ANONYMOUS_USERNAME => pht('Anonymous Username'),
self::KEY_ANONYMOUS_PASSWORD => pht('Anonymous Password'),
self::KEY_USERNAME_ATTRIBUTE => pht('Username Attribute'),
self::KEY_REALNAME_ATTRIBUTES => pht('Realname Attributes'),
self::KEY_VERSION => pht('LDAP Version'),
self::KEY_REFERRALS => pht('Enable Referrals'),
self::KEY_START_TLS => pht('Use TLS'),
self::KEY_ACTIVEDIRECTORY_DOMAIN => pht('ActiveDirectory Domain'),
);
}
public function readFormValuesFromProvider() {
$properties = array();
foreach ($this->getPropertyLabels() as $key => $ignored) {
$properties[$key] = $this->getProviderConfig()->getProperty($key);
}
return $properties;
}
public function readFormValuesFromRequest(AphrontRequest $request) {
$values = array();
foreach ($this->getPropertyKeys() as $key) {
switch ($key) {
case self::KEY_REALNAME_ATTRIBUTES:
$values[$key] = $request->getStrList($key, array());
break;
default:
$values[$key] = $request->getStr($key);
break;
}
}
return $values;
}
public function processEditForm(
AphrontRequest $request,
array $values) {
$errors = array();
$issues = array();
return array($errors, $issues, $values);
}
public static function assertLDAPExtensionInstalled() {
if (!function_exists('ldap_bind')) {
throw new Exception(
pht(
'Before you can set up or use LDAP, you need to install the PHP '.
'LDAP extension. It is not currently installed, so PHP can not '.
'talk to LDAP. Usually you can install it with '.
'`%s`, `%s`, or a similar package manager command.',
'yum install php-ldap',
'apt-get install php5-ldap'));
}
}
public function extendEditForm(
AphrontRequest $request,
AphrontFormView $form,
array $values,
array $issues) {
self::assertLDAPExtensionInstalled();
$labels = $this->getPropertyLabels();
$captions = array(
self::KEY_HOSTNAME =>
pht('Example: %s%sFor LDAPS, use: %s',
phutil_tag('tt', array(), pht('ldap.example.com')),
phutil_tag('br'),
phutil_tag('tt', array(), pht('ldaps://ldaps.example.com/'))),
self::KEY_DISTINGUISHED_NAME =>
pht('Example: %s',
phutil_tag('tt', array(), pht('ou=People, dc=example, dc=com'))),
self::KEY_USERNAME_ATTRIBUTE =>
pht('Example: %s',
phutil_tag('tt', array(), pht('sn'))),
self::KEY_REALNAME_ATTRIBUTES =>
pht('Example: %s',
phutil_tag('tt', array(), pht('firstname, lastname'))),
self::KEY_REFERRALS =>
pht('Follow referrals. Disable this for Windows AD 2003.'),
self::KEY_START_TLS =>
pht('Start TLS after binding to the LDAP server.'),
self::KEY_ALWAYS_SEARCH =>
pht('Always bind and search, even without a username and password.'),
);
$types = array(
self::KEY_REFERRALS => 'checkbox',
self::KEY_START_TLS => 'checkbox',
self::KEY_SEARCH_ATTRIBUTES => 'textarea',
self::KEY_REALNAME_ATTRIBUTES => 'list',
self::KEY_ANONYMOUS_PASSWORD => 'password',
self::KEY_ALWAYS_SEARCH => 'checkbox',
);
$instructions = array(
self::KEY_SEARCH_ATTRIBUTES => pht(
"When a user types their LDAP username and password into Phabricator, ".
"Phabricator can either bind to LDAP with those credentials directly ".
"(which is simpler, but not as powerful) or bind to LDAP with ".
"anonymous credentials, then search for record matching the supplied ".
"credentials (which is more complicated, but more powerful).\n\n".
"For many installs, direct binding is sufficient. However, you may ".
"want to search first if:\n\n".
" - You want users to be able to log in with either their username ".
" or their email address.\n".
" - The login/username is not part of the distinguished name in ".
" your LDAP records.\n".
" - You want to restrict logins to a subset of users (like only ".
" those in certain departments).\n".
" - Your LDAP server is configured in some other way that prevents ".
" direct binding from working correctly.\n\n".
"**To bind directly**, enter the LDAP attribute corresponding to the ".
"login name into the **Search Attributes** box below. Often, this is ".
"something like `sn` or `uid`. This is the simplest configuration, ".
"but will only work if the username is part of the distinguished ".
"name, and won't let you apply complex restrictions to logins.\n\n".
" lang=text,name=Simple Direct Binding\n".
" sn\n\n".
"**To search first**, provide an anonymous username and password ".
"below (or check the **Always Search** checkbox), then enter one ".
"or more search queries into this field, one per line. ".
"After binding, these queries will be used to identify the ".
"record associated with the login name the user typed.\n\n".
"Searches will be tried in order until a matching record is found. ".
"Each query can be a simple attribute name (like `sn` or `mail`), ".
"which will search for a matching record, or it can be a complex ".
"query that uses the string `\${login}` to represent the login ".
"name.\n\n".
"A common simple configuration is just an attribute name, like ".
"`sn`, which will work the same way direct binding works:\n\n".
" lang=text,name=Simple Example\n".
" sn\n\n".
"A slightly more complex configuration might let the user log in with ".
"either their login name or email address:\n\n".
" lang=text,name=Match Several Attributes\n".
" mail\n".
" sn\n\n".
"If your LDAP directory is more complex, or you want to perform ".
"sophisticated filtering, you can use more complex queries. Depending ".
"on your directory structure, this example might allow users to log ".
"in with either their email address or username, but only if they're ".
"in specific departments:\n\n".
" lang=text,name=Complex Example\n".
" (&(mail=\${login})(|(departmentNumber=1)(departmentNumber=2)))\n".
" (&(sn=\${login})(|(departmentNumber=1)(departmentNumber=2)))\n\n".
"All of the attribute names used here are just examples: your LDAP ".
"server may use different attribute names."),
self::KEY_ALWAYS_SEARCH => pht(
'To search for an LDAP record before authenticating, either check '.
'the **Always Search** checkbox or enter an anonymous '.
'username and password to use to perform the search.'),
self::KEY_USERNAME_ATTRIBUTE => pht(
'Optionally, specify a username attribute to use to prefill usernames '.
'when registering a new account. This is purely cosmetic and does not '.
'affect the login process, but you can configure it to make sure '.
'users get the same default username as their LDAP username, so '.
'usernames remain consistent across systems.'),
self::KEY_REALNAME_ATTRIBUTES => pht(
'Optionally, specify one or more comma-separated attributes to use to '.
'prefill the "Real Name" field when registering a new account. This '.
'is purely cosmetic and does not affect the login process, but can '.
'make registration a little easier.'),
);
foreach ($labels as $key => $label) {
$caption = idx($captions, $key);
$type = idx($types, $key);
$value = idx($values, $key);
$control = null;
switch ($type) {
case 'checkbox':
$control = id(new AphrontFormCheckboxControl())
->addCheckbox(
$key,
1,
hsprintf('<strong>%s:</strong> %s', $label, $caption),
$value);
break;
case 'list':
$control = id(new AphrontFormTextControl())
->setName($key)
->setLabel($label)
->setCaption($caption)
->setValue($value ? implode(', ', $value) : null);
break;
case 'password':
$control = id(new AphrontFormPasswordControl())
->setName($key)
->setLabel($label)
->setCaption($caption)
->setDisableAutocomplete(true)
->setValue($value);
break;
case 'textarea':
$control = id(new AphrontFormTextAreaControl())
->setName($key)
->setLabel($label)
->setCaption($caption)
->setValue($value);
break;
default:
$control = id(new AphrontFormTextControl())
->setName($key)
->setLabel($label)
->setCaption($caption)
->setValue($value);
break;
}
$instruction_text = idx($instructions, $key);
if (strlen($instruction_text)) {
$form->appendRemarkupInstructions($instruction_text);
}
$form->appendChild($control);
}
}
public function renderConfigPropertyTransactionTitle(
PhabricatorAuthProviderConfigTransaction $xaction) {
$author_phid = $xaction->getAuthorPHID();
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$key = $xaction->getMetadataValue(
PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY);
$labels = $this->getPropertyLabels();
if (isset($labels[$key])) {
$label = $labels[$key];
$mask = false;
switch ($key) {
case self::KEY_ANONYMOUS_PASSWORD:
$mask = true;
break;
}
if ($mask) {
return pht(
'%s updated the "%s" value.',
$xaction->renderHandleLink($author_phid),
$label);
}
if ($old === null || $old === '') {
return pht(
'%s set the "%s" value to "%s".',
$xaction->renderHandleLink($author_phid),
$label,
$new);
} else {
return pht(
'%s changed the "%s" value from "%s" to "%s".',
$xaction->renderHandleLink($author_phid),
$label,
$old,
$new);
}
}
return parent::renderConfigPropertyTransactionTitle($xaction);
}
public static function getLDAPProvider() {
$providers = self::getAllEnabledProviders();
foreach ($providers as $provider) {
if ($provider instanceof PhabricatorLDAPAuthProvider) {
return $provider;
}
}
return null;
}
}
diff --git a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php
index febf16893..9c9334bd1 100644
--- a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php
@@ -1,415 +1,407 @@
<?php
final class PhabricatorPasswordAuthProvider extends PhabricatorAuthProvider {
private $adapter;
public function getProviderName() {
return pht('Local Username/Password'); // c4science custo
}
public function getConfigurationHelp() {
return pht(
"(WARNING) Examine the table below for information on how password ".
"hashes will be stored in the database.\n\n".
"(NOTE) You can select a minimum password length by setting ".
"`%s` in configuration.",
'account.minimum-password-length');
}
public function renderConfigurationFooter() {
$hashers = PhabricatorPasswordHasher::getAllHashers();
$hashers = msort($hashers, 'getStrength');
$hashers = array_reverse($hashers);
$yes = phutil_tag(
'strong',
array(
'style' => 'color: #009900',
),
pht('Yes'));
$no = phutil_tag(
'strong',
array(
'style' => 'color: #990000',
),
pht('Not Installed'));
$best_hasher_name = null;
try {
$best_hasher = PhabricatorPasswordHasher::getBestHasher();
$best_hasher_name = $best_hasher->getHashName();
} catch (PhabricatorPasswordHasherUnavailableException $ex) {
// There are no suitable hashers. The user might be able to enable some,
// so we don't want to fatal here. We'll fatal when users try to actually
// use this stuff if it isn't fixed before then. Until then, we just
// don't highlight a row. In practice, at least one hasher should always
// be available.
}
$rows = array();
$rowc = array();
foreach ($hashers as $hasher) {
$is_installed = $hasher->canHashPasswords();
$rows[] = array(
$hasher->getHumanReadableName(),
$hasher->getHashName(),
$hasher->getHumanReadableStrength(),
($is_installed ? $yes : $no),
($is_installed ? null : $hasher->getInstallInstructions()),
);
$rowc[] = ($best_hasher_name == $hasher->getHashName())
? 'highlighted'
: null;
}
$table = new AphrontTableView($rows);
$table->setRowClasses($rowc);
$table->setHeaders(
array(
pht('Algorithm'),
pht('Name'),
pht('Strength'),
pht('Installed'),
pht('Install Instructions'),
));
$table->setColumnClasses(
array(
'',
'',
'',
'',
'wide',
));
$header = id(new PHUIHeaderView())
->setHeader(pht('Password Hash Algorithms'))
->setSubheader(
pht(
'Stronger algorithms are listed first. The highlighted algorithm '.
'will be used when storing new hashes. Older hashes will be '.
'upgraded to the best algorithm over time.'));
return id(new PHUIObjectBoxView())
->setHeader($header)
->setTable($table);
}
public function getDescriptionForCreate() {
return pht(
'Allow users to log in or register using a username and password.');
}
public function getAdapter() {
if (!$this->adapter) {
$adapter = new PhutilEmptyAuthAdapter();
$adapter->setAdapterType('password');
$adapter->setAdapterDomain('self');
$this->adapter = $adapter;
}
return $this->adapter;
}
public function getLoginOrder() {
// Make sure username/password appears first if it is enabled.
return '100-'.$this->getProviderName();
}
public function shouldAllowAccountLink() {
return false;
}
public function shouldAllowAccountUnlink() {
return false;
}
public function isDefaultRegistrationProvider() {
return true;
}
public function buildLoginForm(
PhabricatorAuthStartController $controller) {
$request = $controller->getRequest();
// c4science custo
$attributes = array(
'method' => 'GET',
'uri' => '/auth/login/password:self/',
);
return $this->renderStandardLoginButton($request, 'login', $attributes);
}
public function buildInviteForm(
PhabricatorAuthStartController $controller) {
$request = $controller->getRequest();
$viewer = $request->getViewer();
$form = id(new AphrontFormView())
->setUser($viewer)
->addHiddenInput('invite', true)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Username'))
->setName('username'));
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('Register an Account'))
->appendForm($form)
->setSubmitURI('/auth/register/')
->addSubmitButton(pht('Continue'));
return $dialog;
}
- public function buildLinkForm(
- PhabricatorAuthLinkController $controller) {
+ public function buildLinkForm($controller) {
throw new Exception(pht("Password providers can't be linked."));
}
private function renderPasswordLoginForm(
AphrontRequest $request,
$require_captcha = false,
$captcha_valid = false) {
$viewer = $request->getUser();
$dialog = id(new AphrontDialogView())
->setSubmitURI($this->getLoginURI())
->setUser($viewer)
->setTitle(pht('Log In as external user')) // c4science custo
->addSubmitButton(pht('Log In'));
if ($this->shouldAllowRegistration()) {
$dialog->addCancelButton(
'/auth/register/',
pht('Register New Account'));
}
$dialog->addFooter(
phutil_tag(
'a',
array(
'href' => '/login/email/',
),
pht('Forgot your password?')));
$v_user = nonempty(
$request->getStr('username'),
$request->getCookie(PhabricatorCookies::COOKIE_USERNAME));
$e_user = null;
$e_pass = null;
$e_captcha = null;
$errors = array();
if ($require_captcha && !$captcha_valid) {
if (AphrontFormRecaptchaControl::hasCaptchaResponse($request)) {
$e_captcha = pht('Invalid');
$errors[] = pht('CAPTCHA was not entered correctly.');
} else {
$e_captcha = pht('Required');
$errors[] = pht(
'Too many login failures recently. You must '.
'submit a CAPTCHA with your login request.');
}
} else if ($request->isHTTPPost()) {
// NOTE: This is intentionally vague so as not to disclose whether a
// given username or email is registered.
$e_user = pht('Invalid');
$e_pass = pht('Invalid');
$errors[] = pht('Username or password are incorrect.');
}
if ($errors) {
$errors = id(new PHUIInfoView())->setErrors($errors);
}
$form = id(new PHUIFormLayoutView())
->setFullWidth(true)
->appendChild($errors)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Username or Email'))
->setName('username')
+ ->setAutofocus(true)
->setValue($v_user)
->setError($e_user))
->appendChild(
id(new AphrontFormPasswordControl())
->setLabel(pht('Password'))
->setName('password')
->setError($e_pass));
if ($require_captcha) {
$form->appendChild(
id(new AphrontFormRecaptchaControl())
->setError($e_captcha));
}
$dialog->appendChild($form);
return $dialog;
}
public function processLoginRequest(
PhabricatorAuthLoginController $controller) {
$request = $controller->getRequest();
$viewer = $request->getUser();
$content_source = PhabricatorContentSource::newFromRequest($request);
$captcha_limit = 5;
$hard_limit = 32;
$limit_window = phutil_units('15 minutes in seconds');
$failed_attempts = PhabricatorUserLog::loadRecentEventsFromThisIP(
PhabricatorUserLog::ACTION_LOGIN_FAILURE,
$limit_window);
// If the same remote address has submitted several failed login attempts
// recently, require they provide a CAPTCHA response for new attempts.
$require_captcha = false;
$captcha_valid = false;
if (AphrontFormRecaptchaControl::isRecaptchaEnabled()) {
if (count($failed_attempts) > $captcha_limit) {
$require_captcha = true;
$captcha_valid = AphrontFormRecaptchaControl::processCaptcha($request);
}
}
// If the user has submitted quite a few failed login attempts recently,
// give them a hard limit.
if (count($failed_attempts) > $hard_limit) {
$guidance = array();
$guidance[] = pht(
'Your remote address has failed too many login attempts recently. '.
'Wait a few minutes before trying again.');
$guidance[] = pht(
'If you are unable to log in to your account, you can '.
'[[ /login/email | send a reset link to your email address ]].');
$guidance = implode("\n\n", $guidance);
$dialog = $controller->newDialog()
->setTitle(pht('Too Many Login Attempts'))
->appendChild(new PHUIRemarkupView($viewer, $guidance))
->addCancelButton('/auth/start/', pht('Wait Patiently'));
return array(null, $dialog);
}
$response = null;
$account = null;
$log_user = null;
if ($request->isFormPost()) {
if (!$require_captcha || $captcha_valid) {
$username_or_email = $request->getStr('username');
if (strlen($username_or_email)) {
$user = id(new PhabricatorUser())->loadOneWhere(
'username = %s',
$username_or_email);
if (!$user) {
$user = PhabricatorUser::loadOneWithEmailAddress(
$username_or_email);
}
if ($user) {
$envelope = new PhutilOpaqueEnvelope($request->getStr('password'));
$engine = id(new PhabricatorAuthPasswordEngine())
->setViewer($user)
->setContentSource($content_source)
->setPasswordType(PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT)
->setObject($user);
if ($engine->isValidPassword($envelope)) {
$account = $this->loadOrCreateAccount($user->getPHID());
$log_user = $user;
}
}
}
}
}
if (!$account) {
if ($request->isFormPost()) {
$log = PhabricatorUserLog::initializeNewLog(
null,
$log_user ? $log_user->getPHID() : null,
PhabricatorUserLog::ACTION_LOGIN_FAILURE);
$log->save();
}
$request->clearCookie(PhabricatorCookies::COOKIE_USERNAME);
$response = $controller->buildProviderPageResponse(
$this,
$this->renderPasswordLoginForm(
$request,
$require_captcha,
$captcha_valid));
}
return array($account, $response);
}
public function shouldRequireRegistrationPassword() {
return true;
}
- public function getDefaultExternalAccount() {
- $adapter = $this->getAdapter();
-
- return id(new PhabricatorExternalAccount())
- ->setAccountType($adapter->getAdapterType())
- ->setAccountDomain($adapter->getAdapterDomain());
- }
-
protected function willSaveAccount(PhabricatorExternalAccount $account) {
parent::willSaveAccount($account);
$account->setUserPHID($account->getAccountID());
}
public function willRegisterAccount(PhabricatorExternalAccount $account) {
parent::willRegisterAccount($account);
$account->setAccountID($account->getUserPHID());
}
public static function getPasswordProvider() {
$providers = self::getAllEnabledProviders();
foreach ($providers as $provider) {
if ($provider instanceof PhabricatorPasswordAuthProvider) {
return $provider;
}
}
return null;
}
public function willRenderLinkedAccount(
PhabricatorUser $viewer,
PHUIObjectItemView $item,
PhabricatorExternalAccount $account) {
return;
}
public function shouldAllowAccountRefresh() {
return false;
}
public function shouldAllowEmailTrustConfiguration() {
return false;
}
// c4science custo
public function isLoginFormAButton() {
return true;
}
}
diff --git a/src/applications/auth/query/PhabricatorAuthProviderConfigQuery.php b/src/applications/auth/query/PhabricatorAuthProviderConfigQuery.php
index 626c80348..ee073e3ac 100644
--- a/src/applications/auth/query/PhabricatorAuthProviderConfigQuery.php
+++ b/src/applications/auth/query/PhabricatorAuthProviderConfigQuery.php
@@ -1,103 +1,90 @@
<?php
final class PhabricatorAuthProviderConfigQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $providerClasses;
-
- const STATUS_ALL = 'status:all';
- const STATUS_ENABLED = 'status:enabled';
-
- private $status = self::STATUS_ALL;
+ private $isEnabled;
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
- public function withStatus($status) {
- $this->status = $status;
+ public function withProviderClasses(array $classes) {
+ $this->providerClasses = $classes;
return $this;
}
- public function withProviderClasses(array $classes) {
- $this->providerClasses = $classes;
+ public function withIsEnabled($is_enabled) {
+ $this->isEnabled = $is_enabled;
return $this;
}
- public static function getStatusOptions() {
- return array(
- self::STATUS_ALL => pht('All Providers'),
- self::STATUS_ENABLED => pht('Enabled Providers'),
- );
+ public function newResultObject() {
+ return new PhabricatorAuthProviderConfig();
}
protected function loadPage() {
- $table = new PhabricatorAuthProviderConfig();
- $conn_r = $table->establishConnection('r');
-
- $data = queryfx_all(
- $conn_r,
- 'SELECT * FROM %T %Q %Q %Q',
- $table->getTableName(),
- $this->buildWhereClause($conn_r),
- $this->buildOrderClause($conn_r),
- $this->buildLimitClause($conn_r));
-
- return $table->loadAllFromArray($data);
+ return $this->loadStandardPage($this->newResultObject());
}
- protected function buildWhereClause(AphrontDatabaseConnection $conn) {
- $where = array();
+ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+ $where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->providerClasses !== null) {
$where[] = qsprintf(
$conn,
'providerClass IN (%Ls)',
$this->providerClasses);
}
- $status = $this->status;
- switch ($status) {
- case self::STATUS_ALL:
- break;
- case self::STATUS_ENABLED:
- $where[] = qsprintf(
- $conn,
- 'isEnabled = 1');
- break;
- default:
- throw new Exception(pht("Unknown status '%s'!", $status));
+ if ($this->isEnabled !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'isEnabled = %d',
+ (int)$this->isEnabled);
}
- $where[] = $this->buildPagingClause($conn);
+ return $where;
+ }
+
+ protected function willFilterPage(array $configs) {
+
+ foreach ($configs as $key => $config) {
+ $provider = $config->getProvider();
+ if (!$provider) {
+ unset($configs[$key]);
+ continue;
+ }
+ }
- return $this->formatWhereClause($conn, $where);
+ return $configs;
}
public function getQueryApplicationClass() {
return 'PhabricatorAuthApplication';
}
}
diff --git a/src/applications/auth/query/PhabricatorExternalAccountQuery.php b/src/applications/auth/query/PhabricatorExternalAccountQuery.php
index b34199ce6..1c5b3fe14 100644
--- a/src/applications/auth/query/PhabricatorExternalAccountQuery.php
+++ b/src/applications/auth/query/PhabricatorExternalAccountQuery.php
@@ -1,202 +1,204 @@
<?php
/**
* NOTE: When loading ExternalAccounts for use in an authentication context
* (that is, you're going to act as the account or link identities or anything
* like that) you should require CAN_EDIT capability even if you aren't actually
* editing the ExternalAccount.
*
* ExternalAccounts have a permissive CAN_VIEW policy (like users) because they
* interact directly with objects and can leave comments, sign documents, etc.
* However, CAN_EDIT is restricted to users who own the accounts.
*/
final class PhabricatorExternalAccountQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $accountTypes;
private $accountDomains;
private $accountIDs;
private $userPHIDs;
private $needImages;
private $accountSecrets;
+ private $providerConfigPHIDs;
public function withUserPHIDs(array $user_phids) {
$this->userPHIDs = $user_phids;
return $this;
}
public function withAccountIDs(array $account_ids) {
$this->accountIDs = $account_ids;
return $this;
}
public function withAccountDomains(array $account_domains) {
$this->accountDomains = $account_domains;
return $this;
}
public function withAccountTypes(array $account_types) {
$this->accountTypes = $account_types;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withIDs($ids) {
$this->ids = $ids;
return $this;
}
public function withAccountSecrets(array $secrets) {
$this->accountSecrets = $secrets;
return $this;
}
public function needImages($need) {
$this->needImages = $need;
return $this;
}
+ public function withProviderConfigPHIDs(array $phids) {
+ $this->providerConfigPHIDs = $phids;
+ return $this;
+ }
+
public function newResultObject() {
return new PhabricatorExternalAccount();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $accounts) {
+ $viewer = $this->getViewer();
+
+ $configs = id(new PhabricatorAuthProviderConfigQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(mpull($accounts, 'getProviderConfigPHID'))
+ ->execute();
+ $configs = mpull($configs, null, 'getPHID');
+
+ foreach ($accounts as $key => $account) {
+ $config_phid = $account->getProviderConfigPHID();
+ $config = idx($configs, $config_phid);
+
+ if (!$config) {
+ unset($accounts[$key]);
+ continue;
+ }
+
+ $account->attachProviderConfig($config);
+ }
+
if ($this->needImages) {
$file_phids = mpull($accounts, 'getProfileImagePHID');
$file_phids = array_filter($file_phids);
if ($file_phids) {
// NOTE: We use the omnipotent viewer here because these files are
// usually created during registration and can't be associated with
// the correct policies, since the relevant user account does not exist
// yet. In effect, if you can see an ExternalAccount, you can see its
// profile image.
$files = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
} else {
$files = array();
}
$default_file = null;
foreach ($accounts as $account) {
$image_phid = $account->getProfileImagePHID();
if ($image_phid && isset($files[$image_phid])) {
$account->attachProfileImageFile($files[$image_phid]);
} else {
if ($default_file === null) {
$default_file = PhabricatorFile::loadBuiltin(
$this->getViewer(),
'profile.png');
}
$account->attachProfileImageFile($default_file);
}
}
}
return $accounts;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->accountTypes !== null) {
$where[] = qsprintf(
$conn,
'accountType IN (%Ls)',
$this->accountTypes);
}
if ($this->accountDomains !== null) {
$where[] = qsprintf(
$conn,
'accountDomain IN (%Ls)',
$this->accountDomains);
}
if ($this->accountIDs !== null) {
$where[] = qsprintf(
$conn,
'accountID IN (%Ls)',
$this->accountIDs);
}
if ($this->userPHIDs !== null) {
$where[] = qsprintf(
$conn,
'userPHID IN (%Ls)',
$this->userPHIDs);
}
if ($this->accountSecrets !== null) {
$where[] = qsprintf(
$conn,
'accountSecret IN (%Ls)',
$this->accountSecrets);
}
+ if ($this->providerConfigPHIDs !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'providerConfigPHID IN (%Ls)',
+ $this->providerConfigPHIDs);
+ }
+
return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorPeopleApplication';
}
- /**
- * Attempts to find an external account and if none exists creates a new
- * external account with a shiny new ID and PHID.
- *
- * NOTE: This function assumes the first item in various query parameters is
- * the correct value to use in creating a new external account.
- */
- public function loadOneOrCreate() {
- $account = $this->executeOne();
- if (!$account) {
- $account = new PhabricatorExternalAccount();
- if ($this->accountIDs) {
- $account->setAccountID(reset($this->accountIDs));
- }
- if ($this->accountTypes) {
- $account->setAccountType(reset($this->accountTypes));
- }
- if ($this->accountDomains) {
- $account->setAccountDomain(reset($this->accountDomains));
- }
- if ($this->accountSecrets) {
- $account->setAccountSecret(reset($this->accountSecrets));
- }
- if ($this->userPHIDs) {
- $account->setUserPHID(reset($this->userPHIDs));
- }
- $account->save();
- }
- return $account;
- }
-
}
diff --git a/src/applications/auth/storage/PhabricatorAuthChallenge.php b/src/applications/auth/storage/PhabricatorAuthChallenge.php
index 8fa07d712..0b740e5fa 100644
--- a/src/applications/auth/storage/PhabricatorAuthChallenge.php
+++ b/src/applications/auth/storage/PhabricatorAuthChallenge.php
@@ -1,262 +1,272 @@
<?php
final class PhabricatorAuthChallenge
extends PhabricatorAuthDAO
implements PhabricatorPolicyInterface {
protected $userPHID;
protected $factorPHID;
protected $sessionPHID;
protected $workflowKey;
protected $challengeKey;
protected $challengeTTL;
protected $responseDigest;
protected $responseTTL;
protected $isCompleted;
protected $properties = array();
private $responseToken;
+ private $isNewChallenge;
const HTTPKEY = '__hisec.challenges__';
const TOKEN_DIGEST_KEY = 'auth.challenge.token';
public static function initializeNewChallenge() {
return id(new self())
->setIsCompleted(0);
}
public static function newHTTPParametersFromChallenges(array $challenges) {
assert_instances_of($challenges, __CLASS__);
$token_list = array();
foreach ($challenges as $challenge) {
$token = $challenge->getResponseToken();
if ($token) {
$token_list[] = sprintf(
'%s:%s',
$challenge->getPHID(),
$token->openEnvelope());
}
}
if (!$token_list) {
return array();
}
$token_list = implode(' ', $token_list);
return array(
self::HTTPKEY => $token_list,
);
}
public static function newChallengeResponsesFromRequest(
array $challenges,
AphrontRequest $request) {
assert_instances_of($challenges, __CLASS__);
$token_list = $request->getStr(self::HTTPKEY);
$token_list = explode(' ', $token_list);
$token_map = array();
foreach ($token_list as $token_element) {
$token_element = trim($token_element, ' ');
if (!strlen($token_element)) {
continue;
}
// NOTE: This error message is intentionally not printing the token to
// avoid disclosing it. As a result, it isn't terribly useful, but no
// normal user should ever end up here.
if (!preg_match('/^[^:]+:/', $token_element)) {
throw new Exception(
pht(
'This request included an improperly formatted MFA challenge '.
'token and can not be processed.'));
}
list($phid, $token) = explode(':', $token_element, 2);
if (isset($token_map[$phid])) {
throw new Exception(
pht(
'This request improperly specifies an MFA challenge token ("%s") '.
'multiple times and can not be processed.',
$phid));
}
$token_map[$phid] = new PhutilOpaqueEnvelope($token);
}
$challenges = mpull($challenges, null, 'getPHID');
$now = PhabricatorTime::getNow();
foreach ($challenges as $challenge_phid => $challenge) {
// If the response window has expired, don't attach the token.
if ($challenge->getResponseTTL() < $now) {
continue;
}
$token = idx($token_map, $challenge_phid);
if (!$token) {
continue;
}
$challenge->setResponseToken($token);
}
}
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'challengeKey' => 'text255',
'challengeTTL' => 'epoch',
'workflowKey' => 'text255',
'responseDigest' => 'text255?',
'responseTTL' => 'epoch?',
'isCompleted' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_issued' => array(
'columns' => array('userPHID', 'challengeTTL'),
),
'key_collection' => array(
'columns' => array('challengeTTL'),
),
),
) + parent::getConfiguration();
}
public function getPHIDType() {
return PhabricatorAuthChallengePHIDType::TYPECONST;
}
public function getIsReusedChallenge() {
if ($this->getIsCompleted()) {
return true;
}
if (!$this->getIsAnsweredChallenge()) {
return false;
}
// If the challenge has been answered but the client has provided a token
// proving that they answered it, this is still a valid response.
if ($this->getResponseToken()) {
return false;
}
return true;
}
public function getIsAnsweredChallenge() {
return (bool)$this->getResponseDigest();
}
public function markChallengeAsAnswered($ttl) {
$token = Filesystem::readRandomCharacters(32);
$token = new PhutilOpaqueEnvelope($token);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$this
->setResponseToken($token)
->setResponseTTL($ttl)
->save();
unset($unguarded);
return $this;
}
public function markChallengeAsCompleted() {
return $this
->setIsCompleted(true)
->save();
}
public function setResponseToken(PhutilOpaqueEnvelope $token) {
if (!$this->getUserPHID()) {
throw new PhutilInvalidStateException('setUserPHID');
}
if ($this->responseToken) {
throw new Exception(
pht(
'This challenge already has a response token; you can not '.
'set a new response token.'));
}
if (preg_match('/ /', $token->openEnvelope())) {
throw new Exception(
pht(
'The response token for this challenge is invalid: response '.
'tokens may not include spaces.'));
}
$digest = PhabricatorHash::digestWithNamedKey(
$token->openEnvelope(),
self::TOKEN_DIGEST_KEY);
if ($this->responseDigest !== null) {
if (!phutil_hashes_are_identical($digest, $this->responseDigest)) {
throw new Exception(
pht(
'Invalid response token for this challenge: token digest does '.
'not match stored digest.'));
}
} else {
$this->responseDigest = $digest;
}
$this->responseToken = $token;
return $this;
}
public function getResponseToken() {
return $this->responseToken;
}
public function setResponseDigest($value) {
throw new Exception(
pht(
'You can not set the response digest for a challenge directly. '.
'Instead, set a response token. A response digest will be computed '.
'automatically.'));
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getProperty($key, $default = null) {
return $this->properties[$key];
}
+ public function setIsNewChallenge($is_new_challenge) {
+ $this->isNewChallenge = $is_new_challenge;
+ return $this;
+ }
+
+ public function getIsNewChallenge() {
+ return $this->isNewChallenge;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::POLICY_NOONE;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return ($viewer->getPHID() === $this->getUserPHID());
}
}
diff --git a/src/applications/auth/storage/PhabricatorAuthPasswordTransaction.php b/src/applications/auth/storage/PhabricatorAuthPasswordTransaction.php
index 9d02112df..a8cb6d10a 100644
--- a/src/applications/auth/storage/PhabricatorAuthPasswordTransaction.php
+++ b/src/applications/auth/storage/PhabricatorAuthPasswordTransaction.php
@@ -1,21 +1,17 @@
<?php
final class PhabricatorAuthPasswordTransaction
extends PhabricatorModularTransaction {
public function getApplicationName() {
return 'auth';
}
public function getApplicationTransactionType() {
return PhabricatorAuthPasswordPHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return null;
- }
-
public function getBaseTransactionClass() {
return 'PhabricatorAuthPasswordTransactionType';
}
}
diff --git a/src/applications/auth/storage/PhabricatorAuthProviderConfig.php b/src/applications/auth/storage/PhabricatorAuthProviderConfig.php
index 1de34c407..876a70c2d 100644
--- a/src/applications/auth/storage/PhabricatorAuthProviderConfig.php
+++ b/src/applications/auth/storage/PhabricatorAuthProviderConfig.php
@@ -1,122 +1,143 @@
<?php
final class PhabricatorAuthProviderConfig
extends PhabricatorAuthDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
protected $providerClass;
protected $providerType;
protected $providerDomain;
protected $isEnabled;
protected $shouldAllowLogin = 0;
protected $shouldAllowRegistration = 0;
protected $shouldAllowLink = 0;
protected $shouldAllowUnlink = 0;
protected $shouldTrustEmails = 0;
protected $shouldAutoLogin = 0;
protected $properties = array();
private $provider;
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorAuthAuthProviderPHIDType::TYPECONST);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'isEnabled' => 'bool',
'providerClass' => 'text128',
'providerType' => 'text32',
'providerDomain' => 'text128',
'shouldAllowLogin' => 'bool',
'shouldAllowRegistration' => 'bool',
'shouldAllowLink' => 'bool',
'shouldAllowUnlink' => 'bool',
'shouldTrustEmails' => 'bool',
'shouldAutoLogin' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_provider' => array(
'columns' => array('providerType', 'providerDomain'),
'unique' => true,
),
'key_class' => array(
'columns' => array('providerClass'),
),
),
) + parent::getConfiguration();
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getProvider() {
if (!$this->provider) {
$base = PhabricatorAuthProvider::getAllBaseProviders();
$found = null;
foreach ($base as $provider) {
if (get_class($provider) == $this->providerClass) {
$found = $provider;
break;
}
}
if ($found) {
$this->provider = id(clone $found)->attachProviderConfig($this);
}
}
return $this->provider;
}
+ public function getURI() {
+ return '/auth/config/view/'.$this->getID().'/';
+ }
+
+ public function getObjectName() {
+ return pht('Auth Provider %d', $this->getID());
+ }
+
+ public function getDisplayName() {
+ return $this->getProvider()->getProviderName();
+ }
+
+ public function getSortVector() {
+ return id(new PhutilSortVector())
+ ->addString($this->getDisplayName());
+ }
+
+ public function newIconView() {
+ return $this->getProvider()->newIconView();
+ }
+
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorAuthProviderConfigEditor();
}
public function getApplicationTransactionTemplate() {
return new PhabricatorAuthProviderConfigTransaction();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::POLICY_USER;
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::POLICY_ADMIN;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
}
diff --git a/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php b/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php
index e1453b438..d5a3588d5 100644
--- a/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php
+++ b/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php
@@ -1,178 +1,174 @@
<?php
final class PhabricatorAuthProviderConfigTransaction
extends PhabricatorApplicationTransaction {
const TYPE_ENABLE = 'config:enable';
const TYPE_LOGIN = 'config:login';
const TYPE_REGISTRATION = 'config:registration';
const TYPE_LINK = 'config:link';
const TYPE_UNLINK = 'config:unlink';
const TYPE_TRUST_EMAILS = 'config:trustEmails';
const TYPE_AUTO_LOGIN = 'config:autoLogin';
const TYPE_PROPERTY = 'config:property';
const PROPERTY_KEY = 'auth:property';
private $provider;
public function setProvider(PhabricatorAuthProvider $provider) {
$this->provider = $provider;
return $this;
}
public function getProvider() {
return $this->provider;
}
public function getApplicationName() {
return 'auth';
}
public function getApplicationTransactionType() {
return PhabricatorAuthAuthProviderPHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return null;
- }
-
public function getIcon() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_ENABLE:
if ($new) {
return 'fa-check';
} else {
return 'fa-ban';
}
}
return parent::getIcon();
}
public function getColor() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_ENABLE:
if ($new) {
return 'green';
} else {
return 'indigo';
}
}
return parent::getColor();
}
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_ENABLE:
if ($old === null) {
return pht(
'%s created this provider.',
$this->renderHandleLink($author_phid));
} else if ($new) {
return pht(
'%s enabled this provider.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s disabled this provider.',
$this->renderHandleLink($author_phid));
}
break;
case self::TYPE_LOGIN:
if ($new) {
return pht(
'%s enabled login.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s disabled login.',
$this->renderHandleLink($author_phid));
}
break;
case self::TYPE_REGISTRATION:
if ($new) {
return pht(
'%s enabled registration.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s disabled registration.',
$this->renderHandleLink($author_phid));
}
break;
case self::TYPE_LINK:
if ($new) {
return pht(
'%s enabled account linking.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s disabled account linking.',
$this->renderHandleLink($author_phid));
}
break;
case self::TYPE_UNLINK:
if ($new) {
return pht(
'%s enabled account unlinking.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s disabled account unlinking.',
$this->renderHandleLink($author_phid));
}
break;
case self::TYPE_TRUST_EMAILS:
if ($new) {
return pht(
'%s enabled email trust.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s disabled email trust.',
$this->renderHandleLink($author_phid));
}
break;
case self::TYPE_AUTO_LOGIN:
if ($new) {
return pht(
'%s enabled auto login.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s disabled auto login.',
$this->renderHandleLink($author_phid));
}
break;
case self::TYPE_PROPERTY:
$provider = $this->getProvider();
if ($provider) {
$title = $provider->renderConfigPropertyTransactionTitle($this);
if (strlen($title)) {
return $title;
}
}
return pht(
'%s edited a property of this provider.',
$this->renderHandleLink($author_phid));
break;
}
return parent::getTitle();
}
}
diff --git a/src/applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php b/src/applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php
index bb08310cf..028be1746 100644
--- a/src/applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php
+++ b/src/applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php
@@ -1,59 +1,55 @@
<?php
final class PhabricatorAuthSSHKeyTransaction
extends PhabricatorApplicationTransaction {
const TYPE_NAME = 'sshkey.name';
const TYPE_KEY = 'sshkey.key';
const TYPE_DEACTIVATE = 'sshkey.deactivate';
public function getApplicationName() {
return 'auth';
}
public function getApplicationTransactionType() {
return PhabricatorAuthSSHKeyPHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return null;
- }
-
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CREATE:
return pht(
'%s created this key.',
$this->renderHandleLink($author_phid));
case self::TYPE_NAME:
return pht(
'%s renamed this key from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$old,
$new);
case self::TYPE_KEY:
return pht(
'%s updated the public key material for this SSH key.',
$this->renderHandleLink($author_phid));
case self::TYPE_DEACTIVATE:
if ($new) {
return pht(
'%s revoked this key.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s reinstated this key.',
$this->renderHandleLink($author_phid));
}
}
return parent::getTitle();
}
}
diff --git a/src/applications/auth/view/PhabricatorAuthChallengeUpdate.php b/src/applications/auth/view/PhabricatorAuthChallengeUpdate.php
new file mode 100644
index 000000000..a8ae5b882
--- /dev/null
+++ b/src/applications/auth/view/PhabricatorAuthChallengeUpdate.php
@@ -0,0 +1,44 @@
+<?php
+
+final class PhabricatorAuthChallengeUpdate
+ extends Phobject {
+
+ private $retry = false;
+ private $state;
+ private $markup;
+
+ public function setRetry($retry) {
+ $this->retry = $retry;
+ return $this;
+ }
+
+ public function getRetry() {
+ return $this->retry;
+ }
+
+ public function setState($state) {
+ $this->state = $state;
+ return $this;
+ }
+
+ public function getState() {
+ return $this->state;
+ }
+
+ public function setMarkup($markup) {
+ $this->markup = $markup;
+ return $this;
+ }
+
+ public function getMarkup() {
+ return $this->markup;
+ }
+
+ public function newContent() {
+ return array(
+ 'retry' => $this->getRetry(),
+ 'state' => $this->getState(),
+ 'markup' => $this->getMarkup(),
+ );
+ }
+}
diff --git a/src/applications/badges/query/PhabricatorBadgesQuery.php b/src/applications/badges/query/PhabricatorBadgesQuery.php
index c977e3f82..dcadf881f 100644
--- a/src/applications/badges/query/PhabricatorBadgesQuery.php
+++ b/src/applications/badges/query/PhabricatorBadgesQuery.php
@@ -1,119 +1,119 @@
<?php
final class PhabricatorBadgesQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $qualities;
private $statuses;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withQualities(array $qualities) {
$this->qualities = $qualities;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withNameNgrams($ngrams) {
return $this->withNgramsConstraint(
id(new PhabricatorBadgesBadgeNameNgrams()),
$ngrams);
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function getPrimaryTableAlias() {
return 'badges';
}
public function newResultObject() {
return new PhabricatorBadgesBadge();
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'badges.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'badges.phid IN (%Ls)',
$this->phids);
}
if ($this->qualities !== null) {
$where[] = qsprintf(
$conn,
'badges.quality IN (%Ls)',
$this->qualities);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'badges.status IN (%Ls)',
$this->statuses);
}
return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorBadgesApplication';
}
public function getBuiltinOrders() {
return array(
'quality' => array(
'vector' => array('quality', 'id'),
'name' => pht('Rarity (Rarest First)'),
),
'shoddiness' => array(
'vector' => array('-quality', '-id'),
'name' => pht('Rarity (Most Common First)'),
),
) + parent::getBuiltinOrders();
}
public function getOrderableColumns() {
return array(
'quality' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'quality',
'reverse' => true,
'type' => 'int',
),
) + parent::getOrderableColumns();
}
- protected function getPagingValueMap($cursor, array $keys) {
- $badge = $this->loadCursorObject($cursor);
+
+ protected function newPagingMapFromPartialObject($object) {
return array(
- 'quality' => $badge->getQuality(),
- 'id' => $badge->getID(),
+ 'id' => (int)$object->getID(),
+ 'quality' => $object->getQuality(),
);
}
}
diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php
index 59df22a8a..cfd0eaee6 100644
--- a/src/applications/base/controller/PhabricatorController.php
+++ b/src/applications/base/controller/PhabricatorController.php
@@ -1,635 +1,636 @@
<?php
abstract class PhabricatorController extends AphrontController {
private $handles;
public function shouldRequireLogin() {
return true;
}
public function shouldRequireAdmin() {
return false;
}
public function shouldRequireEnabledUser() {
return true;
}
public function shouldAllowPublic() {
return false;
}
public function shouldAllowPartialSessions() {
return false;
}
public function shouldRequireEmailVerification() {
return PhabricatorUserEmail::isEmailVerificationRequired();
}
public function shouldAllowRestrictedParameter($parameter_name) {
return false;
}
public function shouldRequireMultiFactorEnrollment() {
if (!$this->shouldRequireLogin()) {
return false;
}
if (!$this->shouldRequireEnabledUser()) {
return false;
}
if ($this->shouldAllowPartialSessions()) {
return false;
}
$user = $this->getRequest()->getUser();
if (!$user->getIsStandardUser()) {
return false;
}
return PhabricatorEnv::getEnvConfig('security.require-multi-factor-auth');
}
public function shouldAllowLegallyNonCompliantUsers() {
return false;
}
public function isGlobalDragAndDropUploadEnabled() {
return false;
}
public function willBeginExecution() {
$request = $this->getRequest();
if ($request->getUser()) {
// NOTE: Unit tests can set a user explicitly. Normal requests are not
// permitted to do this.
PhabricatorTestCase::assertExecutingUnitTests();
$user = $request->getUser();
} else {
$user = new PhabricatorUser();
$session_engine = new PhabricatorAuthSessionEngine();
$phsid = $request->getCookie(PhabricatorCookies::COOKIE_SESSION);
if (strlen($phsid)) {
$session_user = $session_engine->loadUserForSession(
PhabricatorAuthSession::TYPE_WEB,
$phsid);
if ($session_user) {
$user = $session_user;
}
} else {
// If the client doesn't have a session token, generate an anonymous
// session. This is used to provide CSRF protection to logged-out users.
$phsid = $session_engine->establishSession(
PhabricatorAuthSession::TYPE_WEB,
null,
$partial = false);
// This may be a resource request, in which case we just don't set
// the cookie.
if ($request->canSetCookies()) {
$request->setCookie(PhabricatorCookies::COOKIE_SESSION, $phsid);
}
}
if (!$user->isLoggedIn()) {
$csrf = PhabricatorHash::digestWithNamedKey($phsid, 'csrf.alternate');
$user->attachAlternateCSRFString($csrf);
}
$request->setUser($user);
}
id(new PhabricatorAuthSessionEngine())
->willServeRequestForUser($user);
if (PhabricatorEnv::getEnvConfig('darkconsole.enabled')) {
$dark_console = PhabricatorDarkConsoleSetting::SETTINGKEY;
if ($user->getUserSetting($dark_console) ||
PhabricatorEnv::getEnvConfig('darkconsole.always-on')) {
$console = new DarkConsoleCore();
$request->getApplicationConfiguration()->setConsole($console);
}
}
// NOTE: We want to set up the user first so we can render a real page
// here, but fire this before any real logic.
$restricted = array(
'code',
);
foreach ($restricted as $parameter) {
if ($request->getExists($parameter)) {
if (!$this->shouldAllowRestrictedParameter($parameter)) {
throw new Exception(
pht(
'Request includes restricted parameter "%s", but this '.
'controller ("%s") does not whitelist it. Refusing to '.
'serve this request because it might be part of a redirection '.
'attack.',
$parameter,
get_class($this)));
}
}
}
if ($this->shouldRequireEnabledUser()) {
if ($user->getIsDisabled()) {
$controller = new PhabricatorDisabledUserController();
return $this->delegateToController($controller);
}
}
$auth_class = 'PhabricatorAuthApplication';
$auth_application = PhabricatorApplication::getByClass($auth_class);
// Require partial sessions to finish login before doing anything.
if (!$this->shouldAllowPartialSessions()) {
if ($user->hasSession() &&
$user->getSession()->getIsPartial()) {
$login_controller = new PhabricatorAuthFinishController();
$this->setCurrentApplication($auth_application);
return $this->delegateToController($login_controller);
}
}
// Require users sign Legalpad documents before we check if they have
// MFA. If we don't do this, they can get stuck in a state where they
// can't add MFA until they sign, and can't sign until they add MFA.
// See T13024 and PHI223.
$result = $this->requireLegalpadSignatures();
if ($result !== null) {
return $result;
}
// Check if the user needs to configure MFA.
$need_mfa = $this->shouldRequireMultiFactorEnrollment();
$have_mfa = $user->getIsEnrolledInMultiFactor();
if ($need_mfa && !$have_mfa) {
// Check if the cache is just out of date. Otherwise, roadblock the user
// and require MFA enrollment.
$user->updateMultiFactorEnrollment();
if (!$user->getIsEnrolledInMultiFactor()) {
$mfa_controller = new PhabricatorAuthNeedsMultiFactorController();
$this->setCurrentApplication($auth_application);
return $this->delegateToController($mfa_controller);
}
}
if ($this->shouldRequireLogin()) {
// This actually means we need either:
// - a valid user, or a public controller; and
// - permission to see the application; and
// - permission to see at least one Space if spaces are configured.
$allow_public = $this->shouldAllowPublic() &&
PhabricatorEnv::getEnvConfig('policy.allow-public');
// If this controller isn't public, and the user isn't logged in, require
// login.
if (!$allow_public && !$user->isLoggedIn()) {
$login_controller = new PhabricatorAuthStartController();
$this->setCurrentApplication($auth_application);
return $this->delegateToController($login_controller);
}
if ($user->isLoggedIn()) {
if ($this->shouldRequireEmailVerification()) {
if (!$user->getIsEmailVerified()) {
$controller = new PhabricatorMustVerifyEmailController();
$this->setCurrentApplication($auth_application);
return $this->delegateToController($controller);
}
}
}
// If Spaces are configured, require that the user have access to at
// least one. If we don't do this, they'll get confusing error messages
// later on.
$spaces = PhabricatorSpacesNamespaceQuery::getSpacesExist();
if ($spaces) {
$viewer_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces(
$user);
if (!$viewer_spaces) {
$controller = new PhabricatorSpacesNoAccessController();
return $this->delegateToController($controller);
}
}
// If the user doesn't have access to the application, don't let them use
// any of its controllers. We query the application in order to generate
// a policy exception if the viewer doesn't have permission.
$application = $this->getCurrentApplication();
if ($application) {
id(new PhabricatorApplicationQuery())
->setViewer($user)
->withPHIDs(array($application->getPHID()))
->executeOne();
}
// If users need approval, require they wait here. We do this near the
// end so they can take other actions (like verifying email, signing
// documents, and enrolling in MFA) while waiting for an admin to take a
// look at things. See T13024 for more discussion.
if ($this->shouldRequireEnabledUser()) {
if ($user->isLoggedIn() && !$user->getIsApproved()) {
$controller = new PhabricatorAuthNeedsApprovalController();
return $this->delegateToController($controller);
}
}
}
// NOTE: We do this last so that users get a login page instead of a 403
// if they need to login.
if ($this->shouldRequireAdmin() && !$user->getIsAdmin()) {
return new Aphront403Response();
}
}
public function getApplicationURI($path = '') {
if (!$this->getCurrentApplication()) {
throw new Exception(pht('No application!'));
}
return $this->getCurrentApplication()->getApplicationURI($path);
}
public function willSendResponse(AphrontResponse $response) {
$request = $this->getRequest();
if ($response instanceof AphrontDialogResponse) {
if (!$request->isAjax() && !$request->isQuicksand()) {
$dialog = $response->getDialog();
$title = $dialog->getTitle();
$short = $dialog->getShortTitle();
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(coalesce($short, $title));
$page_content = array(
$crumbs,
$response->buildResponseString(),
);
$view = id(new PhabricatorStandardPageView())
->setRequest($request)
->setController($this)
->setDeviceReady(true)
->setTitle($title)
->appendChild($page_content);
$response = id(new AphrontWebpageResponse())
->setContent($view->render())
->setHTTPResponseCode($response->getHTTPResponseCode());
} else {
$response->getDialog()->setIsStandalone(true);
return id(new AphrontAjaxResponse())
->setContent(array(
'dialog' => $response->buildResponseString(),
));
}
} else if ($response instanceof AphrontRedirectResponse) {
if ($request->isAjax() || $request->isQuicksand()) {
return id(new AphrontAjaxResponse())
->setContent(
array(
'redirect' => $response->getURI(),
'close' => $response->getCloseDialogBeforeRedirect(),
));
}
}
return $response;
}
/**
* WARNING: Do not call this in new code.
*
* @deprecated See "Handles Technical Documentation".
*/
protected function loadViewerHandles(array $phids) {
return id(new PhabricatorHandleQuery())
->setViewer($this->getRequest()->getUser())
->withPHIDs($phids)
->execute();
}
public function buildApplicationMenu() {
return null;
}
protected function buildApplicationCrumbs() {
$crumbs = array();
$application = $this->getCurrentApplication();
if ($application) {
$icon = $application->getIcon();
if (!$icon) {
$icon = 'fa-puzzle';
}
$crumbs[] = id(new PHUICrumbView())
->setHref($this->getApplicationURI())
->setName($application->getName())
->setIcon($icon);
}
$view = new PHUICrumbsView();
foreach ($crumbs as $crumb) {
$view->addCrumb($crumb);
}
return $view;
}
protected function hasApplicationCapability($capability) {
return PhabricatorPolicyFilter::hasCapability(
$this->getRequest()->getUser(),
$this->getCurrentApplication(),
$capability);
}
protected function requireApplicationCapability($capability) {
PhabricatorPolicyFilter::requireCapability(
$this->getRequest()->getUser(),
$this->getCurrentApplication(),
$capability);
}
protected function explainApplicationCapability(
$capability,
$positive_message,
$negative_message) {
$can_act = $this->hasApplicationCapability($capability);
if ($can_act) {
$message = $positive_message;
$icon_name = 'fa-play-circle-o lightgreytext';
} else {
$message = $negative_message;
$icon_name = 'fa-lock';
}
$icon = id(new PHUIIconView())
->setIcon($icon_name);
require_celerity_resource('policy-css');
$phid = $this->getCurrentApplication()->getPHID();
$explain_uri = "/policy/explain/{$phid}/{$capability}/";
$message = phutil_tag(
'div',
array(
'class' => 'policy-capability-explanation',
),
array(
$icon,
javelin_tag(
'a',
array(
'href' => $explain_uri,
'sigil' => 'workflow',
),
$message),
));
return array($can_act, $message);
}
public function getDefaultResourceSource() {
return 'phabricator';
}
/**
* Create a new @{class:AphrontDialogView} with defaults filled in.
*
* @return AphrontDialogView New dialog.
*/
public function newDialog() {
$submit_uri = new PhutilURI($this->getRequest()->getRequestURI());
$submit_uri = $submit_uri->getPath();
return id(new AphrontDialogView())
->setUser($this->getRequest()->getUser())
->setSubmitURI($submit_uri);
}
public function newPage() {
$page = id(new PhabricatorStandardPageView())
->setRequest($this->getRequest())
->setController($this)
->setDeviceReady(true);
$application = $this->getCurrentApplication();
if ($application) {
$page->setApplicationName($application->getName());
if ($application->getTitleGlyph()) {
$page->setGlyph($application->getTitleGlyph());
}
}
$viewer = $this->getRequest()->getUser();
if ($viewer) {
$page->setUser($viewer);
}
return $page;
}
public function newApplicationMenu() {
return id(new PHUIApplicationMenuView())
->setViewer($this->getViewer());
}
public function newCurtainView($object = null) {
$viewer = $this->getViewer();
$action_id = celerity_generate_unique_node_id();
$action_list = id(new PhabricatorActionListView())
->setViewer($viewer)
->setID($action_id);
// NOTE: Applications (objects of class PhabricatorApplication) can't
// currently be set here, although they don't need any of the extensions
// anyway. This should probably work differently than it does, though.
if ($object) {
if ($object instanceof PhabricatorLiskDAO) {
$action_list->setObject($object);
}
}
$curtain = id(new PHUICurtainView())
->setViewer($viewer)
->setActionList($action_list);
if ($object) {
$panels = PHUICurtainExtension::buildExtensionPanels($viewer, $object);
foreach ($panels as $panel) {
$curtain->addPanel($panel);
}
}
return $curtain;
}
protected function buildTransactionTimeline(
PhabricatorApplicationTransactionInterface $object,
PhabricatorApplicationTransactionQuery $query,
PhabricatorMarkupEngine $engine = null,
$view_data = array()) {
$request = $this->getRequest();
$viewer = $this->getViewer();
$xaction = $object->getApplicationTransactionTemplate();
$pager = id(new AphrontCursorPagerView())
->readFromRequest($request)
->setURI(new PhutilURI(
'/transactions/showolder/'.$object->getPHID().'/'));
$xactions = $query
->setViewer($viewer)
->withObjectPHIDs(array($object->getPHID()))
->needComments(true)
->executeWithCursorPager($pager);
$xactions = array_reverse($xactions);
$timeline_engine = PhabricatorTimelineEngine::newForObject($object)
->setViewer($viewer)
->setTransactions($xactions)
->setViewData($view_data);
$view = $timeline_engine->buildTimelineView();
if ($engine) {
foreach ($xactions as $xaction) {
if ($xaction->getComment()) {
$engine->addObject(
$xaction->getComment(),
PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
}
}
$engine->process();
$view->setMarkupEngine($engine);
}
$timeline = $view
->setPager($pager)
->setQuoteTargetID($this->getRequest()->getStr('quoteTargetID'))
->setQuoteRef($this->getRequest()->getStr('quoteRef'));
return $timeline;
}
public function buildApplicationCrumbsForEditEngine() {
// TODO: This is kind of gross, I'm basically just making this public so
// I can use it in EditEngine. We could do this without making it public
// by using controller delegation, or make it properly public.
return $this->buildApplicationCrumbs();
}
private function requireLegalpadSignatures() {
if (!$this->shouldRequireLogin()) {
return null;
}
if ($this->shouldAllowLegallyNonCompliantUsers()) {
return null;
}
$viewer = $this->getViewer();
if (!$viewer->hasSession()) {
return null;
}
$session = $viewer->getSession();
if ($session->getIsPartial()) {
// If the user hasn't made it through MFA yet, require they survive
// MFA first.
return null;
}
if ($session->getSignedLegalpadDocuments()) {
return null;
}
if (!$viewer->isLoggedIn()) {
return null;
}
$must_sign_docs = array();
$sign_docs = array();
$legalpad_class = 'PhabricatorLegalpadApplication';
$legalpad_installed = PhabricatorApplication::isClassInstalledForViewer(
$legalpad_class,
$viewer);
if ($legalpad_installed) {
$sign_docs = id(new LegalpadDocumentQuery())
->setViewer($viewer)
->withSignatureRequired(1)
->needViewerSignatures(true)
->setOrder('oldest')
->execute();
foreach ($sign_docs as $sign_doc) {
if (!$sign_doc->getUserSignature($viewer->getPHID())) {
$must_sign_docs[] = $sign_doc;
}
}
}
if (!$must_sign_docs) {
// If nothing needs to be signed (either because there are no documents
// which require a signature, or because the user has already signed
// all of them) mark the session as good and continue.
$engine = id(new PhabricatorAuthSessionEngine())
->signLegalpadDocuments($viewer, $sign_docs);
return null;
}
$request = $this->getRequest();
$request->setURIMap(
array(
'id' => head($must_sign_docs)->getID(),
));
$application = PhabricatorApplication::getByClass($legalpad_class);
$this->setCurrentApplication($application);
$controller = new LegalpadDocumentSignController();
+ $controller->setIsSessionGate(true);
return $this->delegateToController($controller);
}
/* -( Deprecated )--------------------------------------------------------- */
/**
* DEPRECATED. Use @{method:newPage}.
*/
public function buildStandardPageView() {
return $this->newPage();
}
/**
* DEPRECATED. Use @{method:newPage}.
*/
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->appendChild($view);
return $page->produceAphrontResponse();
}
}
diff --git a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php
index 9c07d371d..6d92f00a9 100644
--- a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php
+++ b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php
@@ -1,295 +1,295 @@
<?php
final class PhabricatorCalendarImportViewController
extends PhabricatorCalendarController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$import = id(new PhabricatorCalendarImportQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->executeOne();
if (!$import) {
return new Aphront404Response();
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(
pht('Imports'),
'/calendar/import/');
$crumbs->addTextCrumb(pht('Import %d', $import->getID()));
$crumbs->setBorder(true);
$timeline = $this->buildTransactionTimeline(
$import,
new PhabricatorCalendarImportTransactionQuery());
$timeline->setShouldTerminate(true);
$header = $this->buildHeaderView($import);
$curtain = $this->buildCurtain($import);
$details = $this->buildPropertySection($import);
$log_messages = $this->buildLogMessages($import);
$imported_events = $this->buildImportedEvents($import);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setMainColumn(
array(
$log_messages,
$imported_events,
$timeline,
))
->setCurtain($curtain)
->addPropertySection(pht('Details'), $details);
$page_title = pht(
'Import %d %s',
$import->getID(),
$import->getDisplayName());
return $this->newPage()
->setTitle($page_title)
->setCrumbs($crumbs)
->setPageObjectPHIDs(array($import->getPHID()))
->appendChild($view);
}
private function buildHeaderView(
PhabricatorCalendarImport $import) {
$viewer = $this->getViewer();
$id = $import->getID();
if ($import->getIsDisabled()) {
$icon = 'fa-ban';
$color = 'red';
$status = pht('Disabled');
} else {
$icon = 'fa-check';
$color = 'bluegrey';
$status = pht('Active');
}
$header = id(new PHUIHeaderView())
->setViewer($viewer)
->setHeader($import->getDisplayName())
->setStatus($icon, $color, $status)
->setPolicyObject($import);
return $header;
}
private function buildCurtain(PhabricatorCalendarImport $import) {
$viewer = $this->getViewer();
$id = $import->getID();
$curtain = $this->newCurtainView($import);
$engine = $import->getEngine();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$import,
PhabricatorPolicyCapability::CAN_EDIT);
$edit_uri = "import/edit/{$id}/";
$edit_uri = $this->getApplicationURI($edit_uri);
$can_disable = ($can_edit && $engine->canDisable($viewer, $import));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Import'))
->setIcon('fa-pencil')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setHref($edit_uri));
$reload_uri = "import/reload/{$id}/";
$reload_uri = $this->getApplicationURI($reload_uri);
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Reload Import'))
->setIcon('fa-refresh')
->setDisabled(!$can_edit)
->setWorkflow(true)
->setHref($reload_uri));
$disable_uri = "import/disable/{$id}/";
$disable_uri = $this->getApplicationURI($disable_uri);
if ($import->getIsDisabled()) {
$disable_name = pht('Enable Import');
$disable_icon = 'fa-check';
} else {
$disable_name = pht('Disable Import');
$disable_icon = 'fa-ban';
}
$curtain->addAction(
id(new PhabricatorActionView())
->setName($disable_name)
->setIcon($disable_icon)
->setDisabled(!$can_disable)
->setWorkflow(true)
->setHref($disable_uri));
if ($can_edit) {
$can_delete = $engine->canDeleteAnyEvents($viewer, $import);
} else {
$can_delete = false;
}
$delete_uri = "import/delete/{$id}/";
$delete_uri = $this->getApplicationURI($delete_uri);
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Delete Imported Events'))
->setIcon('fa-times')
->setDisabled(!$can_delete)
->setWorkflow(true)
->setHref($delete_uri));
return $curtain;
}
private function buildPropertySection(
PhabricatorCalendarImport $import) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setViewer($viewer);
$engine = $import->getEngine();
$properties->addProperty(
pht('Source Type'),
$engine->getImportEngineTypeName());
if ($import->getIsDisabled()) {
$auto_updates = phutil_tag('em', array(), pht('Import Disabled'));
$has_trigger = false;
} else {
$frequency = $import->getTriggerFrequency();
$frequency_map = PhabricatorCalendarImport::getTriggerFrequencyMap();
$frequency_names = ipull($frequency_map, 'name');
$auto_updates = idx($frequency_names, $frequency, $frequency);
if ($frequency == PhabricatorCalendarImport::FREQUENCY_ONCE) {
$has_trigger = false;
$auto_updates = phutil_tag('em', array(), $auto_updates);
} else {
$has_trigger = true;
}
}
$properties->addProperty(
pht('Automatic Updates'),
$auto_updates);
if ($has_trigger) {
$trigger = id(new PhabricatorWorkerTriggerQuery())
->setViewer($viewer)
->withPHIDs(array($import->getTriggerPHID()))
->needEvents(true)
->executeOne();
if (!$trigger) {
$next_trigger = phutil_tag('em', array(), pht('Invalid Trigger'));
} else {
$now = PhabricatorTime::getNow();
$next_epoch = $trigger->getNextEventPrediction();
$next_trigger = pht(
'%s (%s)',
phabricator_datetime($next_epoch, $viewer),
phutil_format_relative_time($next_epoch - $now));
}
$properties->addProperty(
pht('Next Update'),
$next_trigger);
}
$engine->appendImportProperties(
$viewer,
$import,
$properties);
return $properties;
}
private function buildLogMessages(PhabricatorCalendarImport $import) {
$viewer = $this->getViewer();
$logs = id(new PhabricatorCalendarImportLogQuery())
->setViewer($viewer)
->withImportPHIDs(array($import->getPHID()))
->setLimit(25)
->execute();
$logs_view = id(new PhabricatorCalendarImportLogView())
->setViewer($viewer)
->setLogs($logs);
$all_uri = $this->getApplicationURI('import/log/');
$all_uri = (string)id(new PhutilURI($all_uri))
- ->setQueryParam('importSourcePHID', $import->getPHID());
+ ->replaceQueryParam('importSourcePHID', $import->getPHID());
$all_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('View All'))
->setIcon('fa-search')
->setHref($all_uri);
$header = id(new PHUIHeaderView())
->setHeader(pht('Log Messages'))
->addActionLink($all_button);
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($logs_view);
}
private function buildImportedEvents(PhabricatorCalendarImport $import) {
$viewer = $this->getViewer();
$engine = id(new PhabricatorCalendarEventSearchEngine())
->setViewer($viewer);
$saved = $engine->newSavedQuery()
->setParameter('importSourcePHIDs', array($import->getPHID()));
$pager = $engine->newPagerForSavedQuery($saved);
$pager->setPageSize(25);
$query = $engine->buildQueryFromSavedQuery($saved);
$results = $engine->executeQuery($query, $pager);
$view = $engine->renderResults($results, $saved);
$list = $view->getObjectList();
$list->setNoDataString(pht('No imported events.'));
$all_uri = $this->getApplicationURI();
$all_uri = (string)id(new PhutilURI($all_uri))
- ->setQueryParam('importSourcePHID', $import->getPHID())
- ->setQueryParam('display', 'list');
+ ->replaceQueryParam('importSourcePHID', $import->getPHID())
+ ->replaceQueryParam('display', 'list');
$all_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('View All'))
->setIcon('fa-search')
->setHref($all_uri);
$header = id(new PHUIHeaderView())
->setHeader(pht('Imported Events'))
->addActionLink($all_button);
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList($list);
}
}
diff --git a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
index fc1399fdb..db50bb4d7 100644
--- a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
+++ b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
@@ -1,748 +1,747 @@
<?php
final class PhabricatorCalendarEventQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $rangeBegin;
private $rangeEnd;
private $inviteePHIDs;
private $hostPHIDs;
private $isCancelled;
private $eventsWithNoParent;
private $instanceSequencePairs;
private $isStub;
private $parentEventPHIDs;
private $importSourcePHIDs;
private $importAuthorPHIDs;
private $importUIDs;
private $utcInitialEpochMin;
private $utcInitialEpochMax;
private $isImported;
private $needRSVPs;
private $generateGhosts = false;
public function newResultObject() {
return new PhabricatorCalendarEvent();
}
public function setGenerateGhosts($generate_ghosts) {
$this->generateGhosts = $generate_ghosts;
return $this;
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withDateRange($begin, $end) {
$this->rangeBegin = $begin;
$this->rangeEnd = $end;
return $this;
}
public function withUTCInitialEpochBetween($min, $max) {
$this->utcInitialEpochMin = $min;
$this->utcInitialEpochMax = $max;
return $this;
}
public function withInvitedPHIDs(array $phids) {
$this->inviteePHIDs = $phids;
return $this;
}
public function withHostPHIDs(array $phids) {
$this->hostPHIDs = $phids;
return $this;
}
public function withIsCancelled($is_cancelled) {
$this->isCancelled = $is_cancelled;
return $this;
}
public function withIsStub($is_stub) {
$this->isStub = $is_stub;
return $this;
}
public function withEventsWithNoParent($events_with_no_parent) {
$this->eventsWithNoParent = $events_with_no_parent;
return $this;
}
public function withInstanceSequencePairs(array $pairs) {
$this->instanceSequencePairs = $pairs;
return $this;
}
public function withParentEventPHIDs(array $parent_phids) {
$this->parentEventPHIDs = $parent_phids;
return $this;
}
public function withImportSourcePHIDs(array $import_phids) {
$this->importSourcePHIDs = $import_phids;
return $this;
}
public function withImportAuthorPHIDs(array $author_phids) {
$this->importAuthorPHIDs = $author_phids;
return $this;
}
public function withImportUIDs(array $uids) {
$this->importUIDs = $uids;
return $this;
}
public function withIsImported($is_imported) {
$this->isImported = $is_imported;
return $this;
}
public function needRSVPs(array $phids) {
$this->needRSVPs = $phids;
return $this;
}
protected function getDefaultOrderVector() {
return array('start', 'id');
}
public function getBuiltinOrders() {
return array(
'start' => array(
'vector' => array('start', 'id'),
'name' => pht('Event Start'),
),
) + parent::getBuiltinOrders();
}
public function getOrderableColumns() {
return array(
'start' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'utcInitialEpoch',
'reverse' => true,
'type' => 'int',
'unique' => false,
),
) + parent::getOrderableColumns();
}
- protected function getPagingValueMap($cursor, array $keys) {
- $event = $this->loadCursorObject($cursor);
+ protected function newPagingMapFromPartialObject($object) {
return array(
- 'start' => $event->getStartDateTimeEpoch(),
- 'id' => $event->getID(),
+ 'id' => (int)$object->getID(),
+ 'start' => (int)$object->getStartDateTimeEpoch(),
);
}
protected function shouldLimitResults() {
// When generating ghosts, we can't rely on database ordering because
// MySQL can't predict the ghost start times. We'll just load all matching
// events, then generate results from there.
if ($this->generateGhosts) {
return false;
}
return true;
}
protected function loadPage() {
$events = $this->loadStandardPage($this->newResultObject());
$viewer = $this->getViewer();
foreach ($events as $event) {
$event->applyViewerTimezone($viewer);
}
if (!$this->generateGhosts) {
return $events;
}
$raw_limit = $this->getRawResultLimit();
if (!$raw_limit && !$this->rangeEnd) {
throw new Exception(
pht(
'Event queries which generate ghost events must include either a '.
'result limit or an end date, because they may otherwise generate '.
'an infinite number of results. This query has neither.'));
}
foreach ($events as $key => $event) {
$sequence_start = 0;
$sequence_end = null;
$end = null;
$instance_of = $event->getInstanceOfEventPHID();
if ($instance_of == null && $this->isCancelled !== null) {
if ($event->getIsCancelled() != $this->isCancelled) {
unset($events[$key]);
continue;
}
}
}
// Pull out all of the parents first. We may discard them as we begin
// generating ghost events, but we still want to process all of them.
$parents = array();
foreach ($events as $key => $event) {
if ($event->isParentEvent()) {
$parents[$key] = $event;
}
}
// Now that we've picked out all the parent events, we can immediately
// discard anything outside of the time window.
$events = $this->getEventsInRange($events);
$generate_from = $this->rangeBegin;
$generate_until = $this->rangeEnd;
foreach ($parents as $key => $event) {
$duration = $event->getDuration();
$start_date = $this->getRecurrenceWindowStart(
$event,
$generate_from - $duration);
$end_date = $this->getRecurrenceWindowEnd(
$event,
$generate_until);
$limit = $this->getRecurrenceLimit($event, $raw_limit);
$set = $event->newRecurrenceSet();
$recurrences = $set->getEventsBetween(
$start_date,
$end_date,
$limit + 1);
// We're generating events from the beginning and then filtering them
// here (instead of only generating events starting at the start date)
// because we need to know the proper sequence indexes to generate ghost
// events. This may change after RDATE support.
if ($start_date) {
$start_epoch = $start_date->getEpoch();
} else {
$start_epoch = null;
}
foreach ($recurrences as $sequence_index => $sequence_datetime) {
if (!$sequence_index) {
// This is the parent event, which we already have.
continue;
}
if ($start_epoch) {
if ($sequence_datetime->getEpoch() < $start_epoch) {
continue;
}
}
$events[] = $event->newGhost(
$viewer,
$sequence_index,
$sequence_datetime);
}
// NOTE: We're slicing results every time because this makes it cheaper
// to generate future ghosts. If we already have 100 events that occur
// before July 1, we know we never need to generate ghosts after that
// because they couldn't possibly ever appear in the result set.
if ($raw_limit) {
if (count($events) > $raw_limit) {
$events = msort($events, 'getStartDateTimeEpoch');
$events = array_slice($events, 0, $raw_limit, true);
$generate_until = last($events)->getEndDateTimeEpoch();
}
}
}
// Now that we're done generating ghost events, we're going to remove any
// ghosts that we have concrete events for (or which we can load the
// concrete events for). These concrete events are generated when users
// edit a ghost, and replace the ghost events.
// First, generate a map of all concrete <parentPHID, sequence> events we
// already loaded. We don't need to load these again.
$have_pairs = array();
foreach ($events as $event) {
if ($event->getIsGhostEvent()) {
continue;
}
$parent_phid = $event->getInstanceOfEventPHID();
$sequence = $event->getSequenceIndex();
$have_pairs[$parent_phid][$sequence] = true;
}
// Now, generate a map of all <parentPHID, sequence> events we generated
// ghosts for. We need to try to load these if we don't already have them.
$map = array();
$parent_pairs = array();
foreach ($events as $key => $event) {
if (!$event->getIsGhostEvent()) {
continue;
}
$parent_phid = $event->getInstanceOfEventPHID();
$sequence = $event->getSequenceIndex();
// We already loaded the concrete version of this event, so we can just
// throw out the ghost and move on.
if (isset($have_pairs[$parent_phid][$sequence])) {
unset($events[$key]);
continue;
}
// We didn't load the concrete version of this event, so we need to
// try to load it if it exists.
$parent_pairs[] = array($parent_phid, $sequence);
$map[$parent_phid][$sequence] = $key;
}
if ($parent_pairs) {
$instances = id(new self())
->setViewer($viewer)
->setParentQuery($this)
->withInstanceSequencePairs($parent_pairs)
->execute();
foreach ($instances as $instance) {
$parent_phid = $instance->getInstanceOfEventPHID();
$sequence = $instance->getSequenceIndex();
$indexes = idx($map, $parent_phid);
$key = idx($indexes, $sequence);
// Replace the ghost with the corresponding concrete event.
$events[$key] = $instance;
}
}
$events = msort($events, 'getStartDateTimeEpoch');
return $events;
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) {
$parts = parent::buildJoinClauseParts($conn_r);
if ($this->inviteePHIDs !== null) {
$parts[] = qsprintf(
$conn_r,
'JOIN %T invitee ON invitee.eventPHID = event.phid
AND invitee.status != %s',
id(new PhabricatorCalendarEventInvitee())->getTableName(),
PhabricatorCalendarEventInvitee::STATUS_UNINVITED);
}
return $parts;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'event.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'event.phid IN (%Ls)',
$this->phids);
}
// NOTE: The date ranges we query for are larger than the requested ranges
// because we need to catch all-day events. We'll refine this range later
// after adjusting the visible range of events we load.
if ($this->rangeBegin) {
$where[] = qsprintf(
$conn,
'(event.utcUntilEpoch >= %d) OR (event.utcUntilEpoch IS NULL)',
$this->rangeBegin - phutil_units('16 hours in seconds'));
}
if ($this->rangeEnd) {
$where[] = qsprintf(
$conn,
'event.utcInitialEpoch <= %d',
$this->rangeEnd + phutil_units('16 hours in seconds'));
}
if ($this->utcInitialEpochMin !== null) {
$where[] = qsprintf(
$conn,
'event.utcInitialEpoch >= %d',
$this->utcInitialEpochMin);
}
if ($this->utcInitialEpochMax !== null) {
$where[] = qsprintf(
$conn,
'event.utcInitialEpoch <= %d',
$this->utcInitialEpochMax);
}
if ($this->inviteePHIDs !== null) {
$where[] = qsprintf(
$conn,
'invitee.inviteePHID IN (%Ls)',
$this->inviteePHIDs);
}
if ($this->hostPHIDs !== null) {
$where[] = qsprintf(
$conn,
'event.hostPHID IN (%Ls)',
$this->hostPHIDs);
}
if ($this->isCancelled !== null) {
$where[] = qsprintf(
$conn,
'event.isCancelled = %d',
(int)$this->isCancelled);
}
if ($this->eventsWithNoParent == true) {
$where[] = qsprintf(
$conn,
'event.instanceOfEventPHID IS NULL');
}
if ($this->instanceSequencePairs !== null) {
$sql = array();
foreach ($this->instanceSequencePairs as $pair) {
$sql[] = qsprintf(
$conn,
'(event.instanceOfEventPHID = %s AND event.sequenceIndex = %d)',
$pair[0],
$pair[1]);
}
$where[] = qsprintf(
$conn,
'%LO',
$sql);
}
if ($this->isStub !== null) {
$where[] = qsprintf(
$conn,
'event.isStub = %d',
(int)$this->isStub);
}
if ($this->parentEventPHIDs !== null) {
$where[] = qsprintf(
$conn,
'event.instanceOfEventPHID IN (%Ls)',
$this->parentEventPHIDs);
}
if ($this->importSourcePHIDs !== null) {
$where[] = qsprintf(
$conn,
'event.importSourcePHID IN (%Ls)',
$this->importSourcePHIDs);
}
if ($this->importAuthorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'event.importAuthorPHID IN (%Ls)',
$this->importAuthorPHIDs);
}
if ($this->importUIDs !== null) {
$where[] = qsprintf(
$conn,
'event.importUID IN (%Ls)',
$this->importUIDs);
}
if ($this->isImported !== null) {
if ($this->isImported) {
$where[] = qsprintf(
$conn,
'event.importSourcePHID IS NOT NULL');
} else {
$where[] = qsprintf(
$conn,
'event.importSourcePHID IS NULL');
}
}
return $where;
}
protected function getPrimaryTableAlias() {
return 'event';
}
protected function shouldGroupQueryResultRows() {
if ($this->inviteePHIDs !== null) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
public function getQueryApplicationClass() {
return 'PhabricatorCalendarApplication';
}
protected function willFilterPage(array $events) {
$instance_of_event_phids = array();
$recurring_events = array();
$viewer = $this->getViewer();
$events = $this->getEventsInRange($events);
$import_phids = array();
foreach ($events as $event) {
$import_phid = $event->getImportSourcePHID();
if ($import_phid !== null) {
$import_phids[$import_phid] = $import_phid;
}
}
if ($import_phids) {
$imports = id(new PhabricatorCalendarImportQuery())
->setParentQuery($this)
->setViewer($viewer)
->withPHIDs($import_phids)
->execute();
$imports = mpull($imports, null, 'getPHID');
} else {
$imports = array();
}
foreach ($events as $key => $event) {
$import_phid = $event->getImportSourcePHID();
if ($import_phid === null) {
$event->attachImportSource(null);
continue;
}
$import = idx($imports, $import_phid);
if (!$import) {
unset($events[$key]);
$this->didRejectResult($event);
continue;
}
$event->attachImportSource($import);
}
$phids = array();
foreach ($events as $event) {
$phids[] = $event->getPHID();
$instance_of = $event->getInstanceOfEventPHID();
if ($instance_of) {
$instance_of_event_phids[] = $instance_of;
}
}
if (count($instance_of_event_phids) > 0) {
$recurring_events = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withPHIDs($instance_of_event_phids)
->withEventsWithNoParent(true)
->execute();
$recurring_events = mpull($recurring_events, null, 'getPHID');
}
if ($events) {
$invitees = id(new PhabricatorCalendarEventInviteeQuery())
->setViewer($viewer)
->withEventPHIDs($phids)
->execute();
$invitees = mgroup($invitees, 'getEventPHID');
} else {
$invitees = array();
}
foreach ($events as $key => $event) {
$event_invitees = idx($invitees, $event->getPHID(), array());
$event->attachInvitees($event_invitees);
$instance_of = $event->getInstanceOfEventPHID();
if (!$instance_of) {
continue;
}
$parent = idx($recurring_events, $instance_of);
// should never get here
if (!$parent) {
unset($events[$key]);
continue;
}
$event->attachParentEvent($parent);
if ($this->isCancelled !== null) {
if ($event->getIsCancelled() != $this->isCancelled) {
unset($events[$key]);
continue;
}
}
}
$events = msort($events, 'getStartDateTimeEpoch');
if ($this->needRSVPs) {
$rsvp_phids = $this->needRSVPs;
$project_type = PhabricatorProjectProjectPHIDType::TYPECONST;
$project_phids = array();
foreach ($events as $event) {
foreach ($event->getInvitees() as $invitee) {
$invitee_phid = $invitee->getInviteePHID();
if (phid_get_type($invitee_phid) == $project_type) {
$project_phids[] = $invitee_phid;
}
}
}
if ($project_phids) {
$member_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST;
$query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($project_phids)
->withEdgeTypes(array($member_type))
->withDestinationPHIDs($rsvp_phids);
$edges = $query->execute();
$project_map = array();
foreach ($edges as $src => $types) {
foreach ($types as $type => $dsts) {
foreach ($dsts as $dst => $edge) {
$project_map[$dst][] = $src;
}
}
}
} else {
$project_map = array();
}
$membership_map = array();
foreach ($rsvp_phids as $rsvp_phid) {
$membership_map[$rsvp_phid] = array();
$membership_map[$rsvp_phid][] = $rsvp_phid;
$project_phids = idx($project_map, $rsvp_phid);
if ($project_phids) {
foreach ($project_phids as $project_phid) {
$membership_map[$rsvp_phid][] = $project_phid;
}
}
}
foreach ($events as $event) {
$invitees = $event->getInvitees();
$invitees = mpull($invitees, null, 'getInviteePHID');
$rsvp_map = array();
foreach ($rsvp_phids as $rsvp_phid) {
$membership_phids = $membership_map[$rsvp_phid];
$rsvps = array_select_keys($invitees, $membership_phids);
$rsvp_map[$rsvp_phid] = $rsvps;
}
$event->attachRSVPs($rsvp_map);
}
}
return $events;
}
private function getEventsInRange(array $events) {
$range_start = $this->rangeBegin;
$range_end = $this->rangeEnd;
foreach ($events as $key => $event) {
$event_start = $event->getStartDateTimeEpoch();
$event_end = $event->getEndDateTimeEpoch();
if ($range_start && $event_end < $range_start) {
unset($events[$key]);
}
if ($range_end && $event_start > $range_end) {
unset($events[$key]);
}
}
return $events;
}
private function getRecurrenceWindowStart(
PhabricatorCalendarEvent $event,
$generate_from) {
if (!$generate_from) {
return null;
}
return PhutilCalendarAbsoluteDateTime::newFromEpoch($generate_from);
}
private function getRecurrenceWindowEnd(
PhabricatorCalendarEvent $event,
$generate_until) {
$end_epochs = array();
if ($generate_until) {
$end_epochs[] = $generate_until;
}
$until_epoch = $event->getUntilDateTimeEpoch();
if ($until_epoch) {
$end_epochs[] = $until_epoch;
}
if (!$end_epochs) {
return null;
}
return PhutilCalendarAbsoluteDateTime::newFromEpoch(min($end_epochs));
}
private function getRecurrenceLimit(
PhabricatorCalendarEvent $event,
$raw_limit) {
$count = $event->getRecurrenceCount();
if ($count && ($count <= $raw_limit)) {
return ($count - 1);
}
return $raw_limit;
}
}
diff --git a/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php b/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php
index 2c6e58da5..b9893f692 100644
--- a/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php
+++ b/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php
@@ -1,321 +1,320 @@
<?php
final class PhabricatorChatLogChannelLogController
extends PhabricatorChatLogController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('channelID');
- $uri = clone $request->getRequestURI();
- $uri->setQueryParams(array());
+ $uri = new PhutilURI($request->getPath());
$pager = new AphrontCursorPagerView();
$pager->setURI($uri);
$pager->setPageSize(250);
$query = id(new PhabricatorChatLogQuery())
->setViewer($viewer)
->withChannelIDs(array($id));
$channel = id(new PhabricatorChatLogChannelQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$channel) {
return new Aphront404Response();
}
list($after, $before, $map) = $this->getPagingParameters($request, $query);
$pager->setAfterID($after);
$pager->setBeforeID($before);
$logs = $query->executeWithCursorPager($pager);
// Show chat logs oldest-first.
$logs = array_reverse($logs);
// Divide all the logs into blocks, where a block is the same author saying
// several things in a row. A block ends when another user speaks, or when
// two minutes pass without the author speaking.
$blocks = array();
$block = null;
$last_author = null;
$last_epoch = null;
foreach ($logs as $log) {
$this_author = $log->getAuthor();
$this_epoch = $log->getEpoch();
// Decide whether we should start a new block or not.
$new_block = ($this_author !== $last_author) ||
($this_epoch - (60 * 2) > $last_epoch);
if ($new_block) {
if ($block) {
$blocks[] = $block;
}
$block = array(
'id' => $log->getID(),
'epoch' => $this_epoch,
'author' => $this_author,
'logs' => array($log),
);
} else {
$block['logs'][] = $log;
}
$last_author = $this_author;
$last_epoch = $this_epoch;
}
if ($block) {
$blocks[] = $block;
}
// Figure out CSS classes for the blocks. We alternate colors between
// lines, and highlight the entire block which contains the target ID or
// date, if applicable.
foreach ($blocks as $key => $block) {
$classes = array();
if ($key % 2) {
$classes[] = 'alternate';
}
$ids = mpull($block['logs'], 'getID', 'getID');
if (array_intersect_key($ids, $map)) {
$classes[] = 'highlight';
}
$blocks[$key]['class'] = $classes ? implode(' ', $classes) : null;
}
require_celerity_resource('phabricator-chatlog-css');
$out = array();
foreach ($blocks as $block) {
$author = $block['author'];
$author = id(new PhutilUTF8StringTruncator())
->setMaximumGlyphs(18)
->truncateString($author);
$author = phutil_tag('td', array('class' => 'author'), $author);
$href = $uri->alter('at', $block['id']);
$timestamp = $block['epoch'];
$timestamp = phabricator_datetime($timestamp, $viewer);
$timestamp = phutil_tag(
'a',
array(
'href' => $href,
'class' => 'timestamp',
),
$timestamp);
$message = mpull($block['logs'], 'getMessage');
$message = implode("\n", $message);
$message = phutil_tag(
'td',
array(
'class' => 'message',
),
array(
$timestamp,
$message,
));
$out[] = phutil_tag(
'tr',
array(
'class' => $block['class'],
),
array(
$author,
$message,
));
}
$links = array();
$first_uri = $pager->getFirstPageURI();
if ($first_uri) {
$links[] = phutil_tag(
'a',
array(
'href' => $first_uri,
),
"\xC2\xAB ".pht('Newest'));
}
$prev_uri = $pager->getPrevPageURI();
if ($prev_uri) {
$links[] = phutil_tag(
'a',
array(
'href' => $prev_uri,
),
"\xE2\x80\xB9 ".pht('Newer'));
}
$next_uri = $pager->getNextPageURI();
if ($next_uri) {
$links[] = phutil_tag(
'a',
array(
'href' => $next_uri,
),
pht('Older')." \xE2\x80\xBA");
}
$pager_bottom = phutil_tag(
'div',
array('class' => 'phabricator-chat-log-pager-bottom'),
$links);
$crumbs = $this
->buildApplicationCrumbs()
->addTextCrumb($channel->getChannelName(), $uri);
$form = id(new AphrontFormView())
->setUser($viewer)
->setMethod('GET')
->setAction($uri)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Date'))
->setName('date')
->setValue($request->getStr('date')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Jump')));
$table = phutil_tag(
'table',
array(
'class' => 'phabricator-chat-log',
),
$out);
$log = phutil_tag(
'div',
array(
'class' => 'phabricator-chat-log-panel',
),
$table);
$jump_link = id(new PHUIButtonView())
->setTag('a')
->setHref('#latest')
->setText(pht('Jump to Bottom'))
->setIcon('fa-arrow-circle-down');
$jump_target = phutil_tag(
'div',
array(
'id' => 'latest',
));
$content = phutil_tag(
'div',
array(
'class' => 'phabricator-chat-log-wrap',
),
array(
$log,
$jump_target,
$pager_bottom,
));
$header = id(new PHUIHeaderView())
->setHeader($channel->getChannelName())
->setSubHeader($channel->getServiceName())
->addActionLink($jump_link);
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->setCollapsed(true)
->appendChild($content);
$box->setShowHide(
pht('Search Dates'),
pht('Hide Dates'),
$form,
'#');
return $this->newPage()
->setTitle(pht('Channel Log'))
->setCrumbs($crumbs)
->appendChild($box);
}
/**
* From request parameters, figure out where we should jump to in the log.
* We jump to either a date or log ID, but load a few lines of context before
* it so the user can see the nearby conversation.
*/
private function getPagingParameters(
AphrontRequest $request,
PhabricatorChatLogQuery $query) {
$viewer = $request->getViewer();
$at_id = $request->getInt('at');
$at_date = $request->getStr('date');
$context_log = null;
$map = array();
$query = clone $query;
$query->setLimit(8);
if ($at_id) {
// Jump to the log in question, and load a few lines of context before
// it.
$context_logs = $query
->setAfterID($at_id)
->execute();
$context_log = last($context_logs);
$map = array(
$at_id => true,
);
} else if ($at_date) {
$timestamp = PhabricatorTime::parseLocalTime($at_date, $viewer);
if ($timestamp) {
$context_logs = $query
->withMaximumEpoch($timestamp)
->execute();
$context_log = last($context_logs);
$target_log = head($context_logs);
if ($target_log) {
$map = array(
$target_log->getID() => true,
);
}
}
}
if ($context_log) {
$after = null;
$before = $context_log->getID() - 1;
} else {
$after = $request->getInt('after');
$before = $request->getInt('before');
}
return array($after, $before, $map);
}
}
diff --git a/src/applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php b/src/applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php
index f9ba48b37..dc241a04b 100644
--- a/src/applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php
+++ b/src/applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php
@@ -1,89 +1,93 @@
<?php
final class PhabricatorConduitCallManagementWorkflow
extends PhabricatorConduitManagementWorkflow {
protected function didConstruct() {
$this
->setName('call')
->setSynopsis(pht('Call a Conduit method..'))
->setArguments(
array(
array(
'name' => 'method',
'param' => 'method',
'help' => pht('Method to call.'),
),
array(
'name' => 'input',
'param' => 'input',
'help' => pht(
'File to read parameters from, or "-" to read from '.
'stdin.'),
),
array(
'name' => 'as',
'param' => 'username',
'help' => pht(
'Execute the call as the given user. (If omitted, the call will '.
'be executed as an omnipotent user.)'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$viewer = $this->getViewer();
$method = $args->getArg('method');
if (!strlen($method)) {
throw new PhutilArgumentUsageException(
pht('Specify a method to call with "--method".'));
}
$input = $args->getArg('input');
if (!strlen($input)) {
throw new PhutilArgumentUsageException(
pht('Specify a file to read parameters from with "--input".'));
}
$as = $args->getArg('as');
if (strlen($as)) {
$actor = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withUsernames(array($as))
->executeOne();
if (!$actor) {
throw new PhutilArgumentUsageException(
pht(
'No such user "%s" exists.',
$as));
}
+
+ // Allow inline generation of user caches for the user we're acting
+ // as, since some calls may read user preferences.
+ $actor->setAllowInlineCacheGeneration(true);
} else {
$actor = $viewer;
}
if ($input === '-') {
fprintf(STDERR, tsprintf("%s\n", pht('Reading input from stdin...')));
$input_json = file_get_contents('php://stdin');
} else {
$input_json = Filesystem::readFile($input);
}
$params = phutil_json_decode($input_json);
$result = id(new ConduitCall($method, $params))
->setUser($actor)
->execute();
$output = array(
'result' => $result,
);
echo tsprintf(
"%B\n",
id(new PhutilJSON())->encodeFormatted($output));
return 0;
}
}
diff --git a/src/applications/conduit/method/ConduitAPIMethod.php b/src/applications/conduit/method/ConduitAPIMethod.php
index 05831a782..0fbfaa2fc 100644
--- a/src/applications/conduit/method/ConduitAPIMethod.php
+++ b/src/applications/conduit/method/ConduitAPIMethod.php
@@ -1,412 +1,427 @@
<?php
/**
* @task info Method Information
* @task status Method Status
* @task pager Paging Results
*/
abstract class ConduitAPIMethod
extends Phobject
implements PhabricatorPolicyInterface {
private $viewer;
const METHOD_STATUS_STABLE = 'stable';
const METHOD_STATUS_UNSTABLE = 'unstable';
const METHOD_STATUS_DEPRECATED = 'deprecated';
const METHOD_STATUS_FROZEN = 'frozen';
const SCOPE_NEVER = 'scope.never';
const SCOPE_ALWAYS = 'scope.always';
/**
* Get a short, human-readable text summary of the method.
*
* @return string Short summary of method.
* @task info
*/
public function getMethodSummary() {
return $this->getMethodDescription();
}
/**
* Get a detailed description of the method.
*
* This method should return remarkup.
*
* @return string Detailed description of the method.
* @task info
*/
abstract public function getMethodDescription();
public function getMethodDocumentation() {
return null;
}
abstract protected function defineParamTypes();
abstract protected function defineReturnType();
protected function defineErrorTypes() {
return array();
}
abstract protected function execute(ConduitAPIRequest $request);
public function isInternalAPI() {
return false;
}
public function getParamTypes() {
$types = $this->defineParamTypes();
$query = $this->newQueryObject();
if ($query) {
$types['order'] = 'optional order';
$types += $this->getPagerParamTypes();
}
return $types;
}
public function getReturnType() {
return $this->defineReturnType();
}
public function getErrorTypes() {
return $this->defineErrorTypes();
}
/**
* This is mostly for compatibility with
* @{class:PhabricatorCursorPagedPolicyAwareQuery}.
*/
public function getID() {
return $this->getAPIMethodName();
}
/**
* Get the status for this method (e.g., stable, unstable or deprecated).
* Should return a METHOD_STATUS_* constant. By default, methods are
* "stable".
*
* @return const METHOD_STATUS_* constant.
* @task status
*/
public function getMethodStatus() {
return self::METHOD_STATUS_STABLE;
}
/**
* Optional description to supplement the method status. In particular, if
* a method is deprecated, you can return a string here describing the reason
* for deprecation and stable alternatives.
*
* @return string|null Description of the method status, if available.
* @task status
*/
public function getMethodStatusDescription() {
return null;
}
public function getErrorDescription($error_code) {
return idx($this->getErrorTypes(), $error_code, pht('Unknown Error'));
}
public function getRequiredScope() {
return self::SCOPE_NEVER;
}
public function executeMethod(ConduitAPIRequest $request) {
$this->setViewer($request->getUser());
return $this->execute($request);
}
abstract public function getAPIMethodName();
/**
* Return a key which sorts methods by application name, then method status,
* then method name.
*/
public function getSortOrder() {
$name = $this->getAPIMethodName();
$map = array(
self::METHOD_STATUS_STABLE => 0,
self::METHOD_STATUS_UNSTABLE => 1,
self::METHOD_STATUS_DEPRECATED => 2,
);
$ord = idx($map, $this->getMethodStatus(), 0);
list($head, $tail) = explode('.', $name, 2);
return "{$head}.{$ord}.{$tail}";
}
public static function getMethodStatusMap() {
$map = array(
self::METHOD_STATUS_STABLE => pht('Stable'),
self::METHOD_STATUS_UNSTABLE => pht('Unstable'),
self::METHOD_STATUS_DEPRECATED => pht('Deprecated'),
);
return $map;
}
public function getApplicationName() {
return head(explode('.', $this->getAPIMethodName(), 2));
}
public static function loadAllConduitMethods() {
return self::newClassMapQuery()->execute();
}
private static function newClassMapQuery() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getAPIMethodName');
}
public static function getConduitMethod($method_name) {
return id(new PhabricatorCachedClassMapQuery())
->setClassMapQuery(self::newClassMapQuery())
->setMapKeyMethod('getAPIMethodName')
->loadClass($method_name);
}
public function shouldRequireAuthentication() {
return true;
}
public function shouldAllowPublic() {
return false;
}
public function shouldAllowUnguardedWrites() {
return false;
}
/**
* Optionally, return a @{class:PhabricatorApplication} which this call is
* part of. The call will be disabled when the application is uninstalled.
*
* @return PhabricatorApplication|null Related application.
*/
public function getApplication() {
return null;
}
protected function formatStringConstants($constants) {
foreach ($constants as $key => $value) {
$constants[$key] = '"'.$value.'"';
}
$constants = implode(', ', $constants);
return 'string-constant<'.$constants.'>';
}
public static function getParameterMetadataKey($key) {
if (strncmp($key, 'api.', 4) === 0) {
// All keys passed beginning with "api." are always metadata keys.
return substr($key, 4);
} else {
switch ($key) {
// These are real keys which always belong to request metadata.
case 'access_token':
case 'scope':
case 'output':
// This is not a real metadata key; it is included here only to
// prevent Conduit methods from defining it.
case '__conduit__':
// This is prevented globally as a blanket defense against OAuth
// redirection attacks. It is included here to stop Conduit methods
// from defining it.
case 'code':
// This is not a real metadata key, but the presence of this
// parameter triggers an alternate request decoding pathway.
case 'params':
return $key;
}
}
return null;
}
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
final public function getViewer() {
return $this->viewer;
}
/* -( Paging Results )----------------------------------------------------- */
/**
* @task pager
*/
protected function getPagerParamTypes() {
return array(
'before' => 'optional string',
'after' => 'optional string',
'limit' => 'optional int (default = 100)',
);
}
/**
* @task pager
*/
protected function newPager(ConduitAPIRequest $request) {
$limit = $request->getValue('limit', 100);
$limit = min(1000, $limit);
$limit = max(1, $limit);
$pager = id(new AphrontCursorPagerView())
->setPageSize($limit);
$before_id = $request->getValue('before');
if ($before_id !== null) {
$pager->setBeforeID($before_id);
}
$after_id = $request->getValue('after');
if ($after_id !== null) {
$pager->setAfterID($after_id);
}
return $pager;
}
/**
* @task pager
*/
protected function addPagerResults(
array $results,
AphrontCursorPagerView $pager) {
$results['cursor'] = array(
'limit' => $pager->getPageSize(),
'after' => $pager->getNextPageID(),
'before' => $pager->getPrevPageID(),
);
return $results;
}
/* -( Implementing Query Methods )----------------------------------------- */
public function newQueryObject() {
return null;
}
protected function newQueryForRequest(ConduitAPIRequest $request) {
$query = $this->newQueryObject();
if (!$query) {
throw new Exception(
pht(
'You can not call newQueryFromRequest() in this method ("%s") '.
'because it does not implement newQueryObject().',
get_class($this)));
}
if (!($query instanceof PhabricatorCursorPagedPolicyAwareQuery)) {
throw new Exception(
pht(
'Call to method newQueryObject() did not return an object of class '.
'"%s".',
'PhabricatorCursorPagedPolicyAwareQuery'));
}
$query->setViewer($request->getUser());
$order = $request->getValue('order');
if ($order !== null) {
if (is_scalar($order)) {
$query->setOrder($order);
} else {
$query->setOrderVector($order);
}
}
return $query;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getPHID() {
return null;
}
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
// Application methods get application visibility; other methods get open
// visibility.
$application = $this->getApplication();
if ($application) {
return $application->getPolicy($capability);
}
return PhabricatorPolicies::getMostOpenPolicy();
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if (!$this->shouldRequireAuthentication()) {
// Make unauthenticated methods universally visible.
return true;
}
return false;
}
protected function hasApplicationCapability(
$capability,
PhabricatorUser $viewer) {
$application = $this->getApplication();
if (!$application) {
return false;
}
return PhabricatorPolicyFilter::hasCapability(
$viewer,
$application,
$capability);
}
protected function requireApplicationCapability(
$capability,
PhabricatorUser $viewer) {
$application = $this->getApplication();
if (!$application) {
return;
}
PhabricatorPolicyFilter::requireCapability(
$viewer,
$this->getApplication(),
$capability);
}
+ final protected function newRemarkupDocumentationView($remarkup) {
+ $viewer = $this->getViewer();
+
+ $view = new PHUIRemarkupView($viewer, $remarkup);
+
+ $view->setRemarkupOptions(
+ array(
+ PHUIRemarkupView::OPTION_PRESERVE_LINEBREAKS => false,
+ ));
+
+ return id(new PHUIBoxView())
+ ->appendChild($view)
+ ->addPadding(PHUI::PADDING_LARGE);
+ }
+
}
diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
index 1c8a593a7..b676063e8 100644
--- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
+++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
@@ -1,428 +1,543 @@
<?php
final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_OTHER;
}
protected function executeChecks() {
$ancient_config = self::getAncientConfig();
$all_keys = PhabricatorEnv::getAllConfigKeys();
$all_keys = array_keys($all_keys);
sort($all_keys);
$defined_keys = PhabricatorApplicationConfigOptions::loadAllOptions();
+ $stack = PhabricatorEnv::getConfigSourceStack();
+ $stack = $stack->getStack();
+
foreach ($all_keys as $key) {
if (isset($defined_keys[$key])) {
continue;
}
if (isset($ancient_config[$key])) {
$summary = pht(
'This option has been removed. You may delete it at your '.
'convenience.');
$message = pht(
"The configuration option '%s' has been removed. You may delete ".
"it at your convenience.".
"\n\n%s",
$key,
$ancient_config[$key]);
$short = pht('Obsolete Config');
$name = pht('Obsolete Configuration Option "%s"', $key);
} else {
$summary = pht('This option is not recognized. It may be misspelled.');
$message = pht(
"The configuration option '%s' is not recognized. It may be ".
"misspelled, or it might have existed in an older version of ".
"Phabricator. It has no effect, and should be corrected or deleted.",
$key);
$short = pht('Unknown Config');
$name = pht('Unknown Configuration Option "%s"', $key);
}
$issue = $this->newIssue('config.unknown.'.$key)
->setShortName($short)
->setName($name)
->setSummary($summary);
- $stack = PhabricatorEnv::getConfigSourceStack();
- $stack = $stack->getStack();
-
$found = array();
$found_local = false;
$found_database = false;
foreach ($stack as $source_key => $source) {
$value = $source->getKeys(array($key));
if ($value) {
$found[] = $source->getName();
if ($source instanceof PhabricatorConfigDatabaseSource) {
$found_database = true;
}
if ($source instanceof PhabricatorConfigLocalSource) {
$found_local = true;
}
}
}
$message = $message."\n\n".pht(
'This configuration value is defined in these %d '.
'configuration source(s): %s.',
count($found),
implode(', ', $found));
$issue->setMessage($message);
if ($found_local) {
$command = csprintf('phabricator/ $ ./bin/config delete %s', $key);
$issue->addCommand($command);
}
if ($found_database) {
$issue->addPhabricatorConfig($key);
}
}
+
+ $options = PhabricatorApplicationConfigOptions::loadAllOptions();
+ foreach ($defined_keys as $key => $value) {
+ $option = idx($options, $key);
+ if (!$option) {
+ continue;
+ }
+
+ if (!$option->getLocked()) {
+ continue;
+ }
+
+ $found_database = false;
+ foreach ($stack as $source_key => $source) {
+ $value = $source->getKeys(array($key));
+ if ($value) {
+ if ($source instanceof PhabricatorConfigDatabaseSource) {
+ $found_database = true;
+ break;
+ }
+ }
+ }
+
+ if (!$found_database) {
+ continue;
+ }
+
+ // NOTE: These are values which we don't let you edit directly, but edit
+ // via other UI workflows. For now, don't raise this warning about them.
+ // In the future, before we stop reading database configuration for
+ // locked values, we either need to add a flag which lets these values
+ // continue reading from the database or move them to some other storage
+ // mechanism.
+ $soft_locks = array(
+ 'phabricator.uninstalled-applications',
+ 'phabricator.application-settings',
+ 'config.ignore-issues',
+ );
+ $soft_locks = array_fuse($soft_locks);
+ if (isset($soft_locks[$key])) {
+ continue;
+ }
+
+ $doc_name = 'Configuration Guide: Locked and Hidden Configuration';
+ $doc_href = PhabricatorEnv::getDoclink($doc_name);
+
+ $set_command = phutil_tag(
+ 'tt',
+ array(),
+ csprintf(
+ 'bin/config set %R <value>',
+ $key));
+
+ $summary = pht(
+ 'Configuration value "%s" is locked, but has a value in the database.',
+ $key);
+ $message = pht(
+ 'The configuration value "%s" is locked (so it can not be edited '.
+ 'from the web UI), but has a database value. Usually, this means '.
+ 'that it was previously not locked, you set it using the web UI, '.
+ 'and it later became locked.'.
+ "\n\n".
+ 'You should copy this configuration value in a local configuration '.
+ 'source (usually by using %s) and then remove it from the database '.
+ 'with the command below.'.
+ "\n\n".
+ 'For more information on locked and hidden configuration, including '.
+ 'details about this setup issue, see %s.'.
+ "\n\n".
+ 'This database value is currently respected, but a future version '.
+ 'of Phabricator will stop respecting database values for locked '.
+ 'configuration options.',
+ $key,
+ $set_command,
+ phutil_tag(
+ 'a',
+ array(
+ 'href' => $doc_href,
+ 'target' => '_blank',
+ ),
+ $doc_name));
+ $command = csprintf(
+ 'phabricator/ $ ./bin/config delete --database %R',
+ $key);
+
+ $this->newIssue('config.locked.'.$key)
+ ->setShortName(pht('Deprecated Config Source'))
+ ->setName(
+ pht(
+ 'Locked Configuration Option "%s" Has Database Value',
+ $key))
+ ->setSummary($summary)
+ ->setMessage($message)
+ ->addCommand($command)
+ ->addPhabricatorConfig($key);
+ }
+
+ if (PhabricatorEnv::getEnvConfig('feed.http-hooks')) {
+ $this->newIssue('config.deprecated.feed.http-hooks')
+ ->setShortName(pht('Feed Hooks Deprecated'))
+ ->setName(pht('Migrate From "feed.http-hooks" to Webhooks'))
+ ->addPhabricatorConfig('feed.http-hooks')
+ ->setMessage(
+ pht(
+ 'The "feed.http-hooks" option is deprecated in favor of '.
+ 'Webhooks. This option will be removed in a future version '.
+ 'of Phabricator.'.
+ "\n\n".
+ 'You can configure Webhooks in Herald.'.
+ "\n\n".
+ 'To resolve this issue, remove all URIs from "feed.http-hooks".'));
+ }
+
}
/**
* Return a map of deleted config options. Keys are option keys; values are
* explanations of what happened to the option.
*/
public static function getAncientConfig() {
$reason_auth = pht(
'This option has been migrated to the "Auth" application. Your old '.
'configuration is still in effect, but now stored in "Auth" instead of '.
'configuration. Going forward, you can manage authentication from '.
'the web UI.');
$auth_config = array(
'controller.oauth-registration',
'auth.password-auth-enabled',
'facebook.auth-enabled',
'facebook.registration-enabled',
'facebook.auth-permanent',
'facebook.application-id',
'facebook.application-secret',
'facebook.require-https-auth',
'github.auth-enabled',
'github.registration-enabled',
'github.auth-permanent',
'github.application-id',
'github.application-secret',
'google.auth-enabled',
'google.registration-enabled',
'google.auth-permanent',
'google.application-id',
'google.application-secret',
'ldap.auth-enabled',
'ldap.hostname',
'ldap.port',
'ldap.base_dn',
'ldap.search_attribute',
'ldap.search-first',
'ldap.username-attribute',
'ldap.real_name_attributes',
'ldap.activedirectory_domain',
'ldap.version',
'ldap.referrals',
'ldap.anonymous-user-name',
'ldap.anonymous-user-password',
'ldap.start-tls',
'disqus.auth-enabled',
'disqus.registration-enabled',
'disqus.auth-permanent',
'disqus.application-id',
'disqus.application-secret',
'phabricator.oauth-uri',
'phabricator.auth-enabled',
'phabricator.registration-enabled',
'phabricator.auth-permanent',
'phabricator.application-id',
'phabricator.application-secret',
);
$ancient_config = array_fill_keys($auth_config, $reason_auth);
$markup_reason = pht(
'Custom remarkup rules are now added by subclassing '.
'%s or %s.',
'PhabricatorRemarkupCustomInlineRule',
'PhabricatorRemarkupCustomBlockRule');
$session_reason = pht(
'Sessions now expire and are garbage collected rather than having an '.
'arbitrary concurrency limit.');
$differential_field_reason = pht(
'All Differential fields are now managed through the configuration '.
'option "%s". Use that option to configure which fields are shown.',
'differential.fields');
$reply_domain_reason = pht(
'Individual application reply handler domains have been removed. '.
'Configure a reply domain with "%s".',
'metamta.reply-handler-domain');
$reply_handler_reason = pht(
'Reply handlers can no longer be overridden with configuration.');
$monospace_reason = pht(
'Phabricator no longer supports global customization of monospaced '.
'fonts.');
$public_mail_reason = pht(
'Inbound mail addresses are now configured for each application '.
'in the Applications tool.');
$gc_reason = pht(
'Garbage collectors are now configured with "%s".',
'bin/garbage set-policy');
$aphlict_reason = pht(
'Configuration of the notification server has changed substantially. '.
'For discussion, see T10794.');
$stale_reason = pht(
'The Differential revision list view age UI elements have been removed '.
'to simplify the interface.');
$global_settings_reason = pht(
'The "Re: Prefix" and "Vary Subjects" settings are now configured '.
'in global settings.');
$dashboard_reason = pht(
'This option has been removed, you can use Dashboards to provide '.
'homepage customization. See T11533 for more details.');
$elastic_reason = pht(
'Elasticsearch is now configured with "%s".',
'cluster.search');
$mailers_reason = pht(
'Inbound and outbound mail is now configured with "cluster.mailers".');
$prefix_reason = pht(
'Per-application mail subject prefix customization is no longer '.
'directly supported. Prefixes and other strings may be customized with '.
'"translation.override".');
$ancient_config += array(
'phid.external-loaders' =>
pht(
'External loaders have been replaced. Extend `%s` '.
'to implement new PHID and handle types.',
'PhabricatorPHIDType'),
'maniphest.custom-task-extensions-class' =>
pht(
'Maniphest fields are now loaded automatically. '.
'You can configure them with `%s`.',
'maniphest.fields'),
'maniphest.custom-fields' =>
pht(
'Maniphest fields are now defined in `%s`. '.
'Existing definitions have been migrated.',
'maniphest.custom-field-definitions'),
'differential.custom-remarkup-rules' => $markup_reason,
'differential.custom-remarkup-block-rules' => $markup_reason,
'auth.sshkeys.enabled' => pht(
'SSH keys are now actually useful, so they are always enabled.'),
'differential.anonymous-access' => pht(
'Phabricator now has meaningful global access controls. See `%s`.',
'policy.allow-public'),
'celerity.resource-path' => pht(
'An alternate resource map is no longer supported. Instead, use '.
'multiple maps. See T4222.'),
'metamta.send-immediately' => pht(
'Mail is now always delivered by the daemons.'),
'auth.sessions.conduit' => $session_reason,
'auth.sessions.web' => $session_reason,
'tokenizer.ondemand' => pht(
'Phabricator now manages typeahead strategies automatically.'),
'differential.revision-custom-detail-renderer' => pht(
'Obsolete; use standard rendering events instead.'),
'differential.show-host-field' => $differential_field_reason,
'differential.show-test-plan-field' => $differential_field_reason,
'differential.field-selector' => $differential_field_reason,
'phabricator.show-beta-applications' => pht(
'This option has been renamed to `%s` to emphasize the '.
'unfinished nature of many prototype applications. '.
'Your existing setting has been migrated.',
'phabricator.show-prototypes'),
'notification.user' => pht(
'The notification server no longer requires root permissions. Start '.
'the server as the user you want it to run under.'),
'notification.debug' => pht(
'Notifications no longer have a dedicated debugging mode.'),
'translation.provider' => pht(
'The translation implementation has changed and providers are no '.
'longer used or supported.'),
'config.mask' => pht(
'Use `%s` instead of this option.',
'config.hide'),
'phd.start-taskmasters' => pht(
'Taskmasters now use an autoscaling pool. You can configure the '.
'pool size with `%s`.',
'phd.taskmasters'),
'storage.engine-selector' => pht(
'Phabricator now automatically discovers available storage engines '.
'at runtime.'),
'storage.upload-size-limit' => pht(
'Phabricator now supports arbitrarily large files. Consult the '.
'documentation for configuration details.'),
'security.allow-outbound-http' => pht(
'This option has been replaced with the more granular option `%s`.',
'security.outbound-blacklist'),
'metamta.reply.show-hints' => pht(
'Phabricator no longer shows reply hints in mail.'),
'metamta.differential.reply-handler-domain' => $reply_domain_reason,
'metamta.diffusion.reply-handler-domain' => $reply_domain_reason,
'metamta.macro.reply-handler-domain' => $reply_domain_reason,
'metamta.maniphest.reply-handler-domain' => $reply_domain_reason,
'metamta.pholio.reply-handler-domain' => $reply_domain_reason,
'metamta.diffusion.reply-handler' => $reply_handler_reason,
'metamta.differential.reply-handler' => $reply_handler_reason,
'metamta.maniphest.reply-handler' => $reply_handler_reason,
'metamta.package.reply-handler' => $reply_handler_reason,
'metamta.precedence-bulk' => pht(
'Phabricator now always sends transaction mail with '.
'"Precedence: bulk" to improve deliverability.'),
'style.monospace' => $monospace_reason,
'style.monospace.windows' => $monospace_reason,
'search.engine-selector' => pht(
'Phabricator now automatically discovers available search engines '.
'at runtime.'),
'metamta.files.public-create-email' => $public_mail_reason,
'metamta.maniphest.public-create-email' => $public_mail_reason,
'metamta.maniphest.default-public-author' => $public_mail_reason,
'metamta.paste.public-create-email' => $public_mail_reason,
'security.allow-conduit-act-as-user' => pht(
'Impersonating users over the API is no longer supported.'),
'feed.public' => pht('The framable public feed is no longer supported.'),
'auth.login-message' => pht(
'This configuration option has been replaced with a modular '.
'handler. See T9346.'),
'gcdaemon.ttl.herald-transcripts' => $gc_reason,
'gcdaemon.ttl.daemon-logs' => $gc_reason,
'gcdaemon.ttl.differential-parse-cache' => $gc_reason,
'gcdaemon.ttl.markup-cache' => $gc_reason,
'gcdaemon.ttl.task-archive' => $gc_reason,
'gcdaemon.ttl.general-cache' => $gc_reason,
'gcdaemon.ttl.conduit-logs' => $gc_reason,
'phd.variant-config' => pht(
'This configuration is no longer relevant because daemons '.
'restart automatically on configuration changes.'),
'notification.ssl-cert' => $aphlict_reason,
'notification.ssl-key' => $aphlict_reason,
'notification.pidfile' => $aphlict_reason,
'notification.log' => $aphlict_reason,
'notification.enabled' => $aphlict_reason,
'notification.client-uri' => $aphlict_reason,
'notification.server-uri' => $aphlict_reason,
'metamta.differential.unified-comment-context' => pht(
'Inline comments are now always rendered with a limited amount '.
'of context.'),
'differential.days-fresh' => $stale_reason,
'differential.days-stale' => $stale_reason,
'metamta.re-prefix' => $global_settings_reason,
'metamta.vary-subjects' => $global_settings_reason,
'ui.custom-header' => pht(
'This option has been replaced with `ui.logo`, which provides more '.
'flexible configuration options.'),
'welcome.html' => $dashboard_reason,
'maniphest.priorities.unbreak-now' => $dashboard_reason,
'maniphest.priorities.needs-triage' => $dashboard_reason,
'mysql.implementation' => pht(
'Phabricator now automatically selects the best available '.
'MySQL implementation.'),
'mysql.configuration-provider' => pht(
'Phabricator now has application-level management of partitioning '.
'and replicas.'),
'search.elastic.host' => $elastic_reason,
'search.elastic.namespace' => $elastic_reason,
'metamta.mail-adapter' => $mailers_reason,
'amazon-ses.access-key' => $mailers_reason,
'amazon-ses.secret-key' => $mailers_reason,
'amazon-ses.endpoint' => $mailers_reason,
'mailgun.domain' => $mailers_reason,
'mailgun.api-key' => $mailers_reason,
'phpmailer.mailer' => $mailers_reason,
'phpmailer.smtp-host' => $mailers_reason,
'phpmailer.smtp-port' => $mailers_reason,
'phpmailer.smtp-protocol' => $mailers_reason,
'phpmailer.smtp-user' => $mailers_reason,
'phpmailer.smtp-password' => $mailers_reason,
'phpmailer.smtp-encoding' => $mailers_reason,
'sendgrid.api-user' => $mailers_reason,
'sendgrid.api-key' => $mailers_reason,
'celerity.resource-hash' => pht(
'This option generally did not prove useful. Resource hash keys '.
'are now managed automatically.'),
'celerity.enable-deflate' => pht(
'Resource deflation is now managed automatically.'),
'celerity.minify' => pht(
'Resource minification is now managed automatically.'),
'metamta.domain' => pht(
'Mail thread IDs are now generated automatically.'),
'metamta.placeholder-to-recipient' => pht(
'Placeholder recipients are now generated automatically.'),
'metamta.mail-key' => pht(
'Mail object address hash keys are now generated automatically.'),
'phabricator.csrf-key' => pht(
'CSRF HMAC keys are now managed automatically.'),
'metamta.insecure-auth-with-reply-to' => pht(
'Authenticating users based on "Reply-To" is no longer supported.'),
'phabricator.allow-email-users' => pht(
'Public email is now accepted if the associated address has a '.
'default author, and rejected otherwise.'),
'metamta.conpherence.subject-prefix' => $prefix_reason,
'metamta.differential.subject-prefix' => $prefix_reason,
'metamta.diffusion.subject-prefix' => $prefix_reason,
'metamta.files.subject-prefix' => $prefix_reason,
'metamta.legalpad.subject-prefix' => $prefix_reason,
'metamta.macro.subject-prefix' => $prefix_reason,
'metamta.maniphest.subject-prefix' => $prefix_reason,
'metamta.package.subject-prefix' => $prefix_reason,
'metamta.paste.subject-prefix' => $prefix_reason,
'metamta.pholio.subject-prefix' => $prefix_reason,
'metamta.phriction.subject-prefix' => $prefix_reason,
'aphront.default-application-configuration-class' => pht(
'This ancient extension point has been replaced with other '.
'mechanisms, including "AphrontSite".'),
+ 'differential.whitespace-matters' => pht(
+ 'Whitespace rendering is now handled automatically.'),
);
return $ancient_config;
}
}
diff --git a/src/applications/config/check/PhabricatorWebServerSetupCheck.php b/src/applications/config/check/PhabricatorWebServerSetupCheck.php
index 398ebd637..284b5e2a5 100644
--- a/src/applications/config/check/PhabricatorWebServerSetupCheck.php
+++ b/src/applications/config/check/PhabricatorWebServerSetupCheck.php
@@ -1,265 +1,264 @@
<?php
final class PhabricatorWebServerSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_OTHER;
}
protected function executeChecks() {
// The documentation says these headers exist, but it's not clear if they
// are entirely reliable in practice.
if (isset($_SERVER['HTTP_X_MOD_PAGESPEED']) ||
isset($_SERVER['HTTP_X_PAGE_SPEED'])) {
$this->newIssue('webserver.pagespeed')
->setName(pht('Disable Pagespeed'))
->setSummary(pht('Pagespeed is enabled, but should be disabled.'))
->setMessage(
pht(
'Phabricator received an "X-Mod-Pagespeed" or "X-Page-Speed" '.
'HTTP header on this request, which indicates that you have '.
'enabled "mod_pagespeed" on this server. This module is not '.
'compatible with Phabricator. You should disable it.'));
}
$base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
if (!strlen($base_uri)) {
// If `phabricator.base-uri` is not set then we can't really do
// anything.
return;
}
$expect_user = 'alincoln';
$expect_pass = 'hunter2';
$send_path = '/test-%252A/';
$expect_path = '/test-%2A/';
$expect_key = 'duck-sound';
$expect_value = 'quack';
$base_uri = id(new PhutilURI($base_uri))
->setPath($send_path)
- ->setQueryParam($expect_key, $expect_value);
+ ->replaceQueryParam($expect_key, $expect_value);
$self_future = id(new HTTPSFuture($base_uri))
->addHeader('X-Phabricator-SelfCheck', 1)
->addHeader('Accept-Encoding', 'gzip')
->setHTTPBasicAuthCredentials(
$expect_user,
new PhutilOpaqueEnvelope($expect_pass))
->setTimeout(5);
// Make a request to the metadata service available on EC2 instances,
// to test if we're running on a T2 instance in AWS so we can warn that
// this is a bad idea. Outside of AWS, this request will just fail.
$ec2_uri = 'http://169.254.169.254/latest/meta-data/instance-type';
$ec2_future = id(new HTTPSFuture($ec2_uri))
->setTimeout(1);
$futures = array(
$self_future,
$ec2_future,
);
$futures = new FutureIterator($futures);
foreach ($futures as $future) {
// Just resolve the futures here.
}
try {
list($body) = $ec2_future->resolvex();
$body = trim($body);
if (preg_match('/^t2/', $body)) {
$message = pht(
'Phabricator appears to be installed on a very small EC2 instance '.
'(of class "%s") with burstable CPU. This is strongly discouraged. '.
'Phabricator regularly needs CPU, and these instances are often '.
'choked to death by CPU throttling. Use an instance with a normal '.
'CPU instead.',
$body);
$this->newIssue('ec2.burstable')
->setName(pht('Installed on Burstable CPU Instance'))
->setSummary(
pht(
'Do not install Phabricator on an instance class with '.
'burstable CPU.'))
->setMessage($message);
}
} catch (Exception $ex) {
// If this fails, just continue. We're probably not running in EC2.
}
try {
list($body, $headers) = $self_future->resolvex();
} catch (Exception $ex) {
// If this fails for whatever reason, just ignore it. Hopefully, the
// error is obvious and the user can correct it on their own, but we
// can't do much to offer diagnostic advice.
return;
}
if (BaseHTTPFuture::getHeader($headers, 'Content-Encoding') != 'gzip') {
$message = pht(
'Phabricator sent itself a request with "Accept-Encoding: gzip", '.
'but received an uncompressed response.'.
"\n\n".
'This may indicate that your webserver is not configured to '.
'compress responses. If so, you should enable compression. '.
'Compression can dramatically improve performance, especially '.
'for clients with less bandwidth.');
$this->newIssue('webserver.gzip')
->setName(pht('GZip Compression May Not Be Enabled'))
->setSummary(pht('Your webserver may have compression disabled.'))
->setMessage($message);
} else {
if (function_exists('gzdecode')) {
$body = gzdecode($body);
} else {
$body = null;
}
if (!$body) {
// For now, just bail if we can't decode the response.
// This might need to use the stronger magic in "AphrontRequestStream"
// to decode more reliably.
return;
}
}
$structure = null;
- $caught = null;
$extra_whitespace = ($body !== trim($body));
- if (!$extra_whitespace) {
- try {
- $structure = phutil_json_decode($body);
- } catch (Exception $ex) {
- $caught = $ex;
- }
+ try {
+ $structure = phutil_json_decode(trim($body));
+ } catch (Exception $ex) {
+ // Ignore the exception, we only care if the decode worked or not.
}
- if (!$structure) {
- if ($extra_whitespace) {
- $message = pht(
- 'Phabricator sent itself a test request and expected to get a bare '.
- 'JSON response back, but the response had extra whitespace at '.
- 'the beginning or end.'.
- "\n\n".
- 'This usually means you have edited a file and left whitespace '.
- 'characters before the opening %s tag, or after a closing %s tag. '.
- 'Remove any leading whitespace, and prefer to omit closing tags.',
- phutil_tag('tt', array(), '<?php'),
- phutil_tag('tt', array(), '?>'));
- } else {
+ if (!$structure || $extra_whitespace) {
+ if (!$structure) {
$short = id(new PhutilUTF8StringTruncator())
->setMaximumGlyphs(1024)
->truncateString($body);
$message = pht(
'Phabricator sent itself a test request with the '.
'"X-Phabricator-SelfCheck" header and expected to get a valid JSON '.
'response back. Instead, the response begins:'.
"\n\n".
'%s'.
"\n\n".
'Something is misconfigured or otherwise mangling responses.',
phutil_tag('pre', array(), $short));
+ } else {
+ $message = pht(
+ 'Phabricator sent itself a test request and expected to get a bare '.
+ 'JSON response back. It received a JSON response, but the response '.
+ 'had extra whitespace at the beginning or end.'.
+ "\n\n".
+ 'This usually means you have edited a file and left whitespace '.
+ 'characters before the opening %s tag, or after a closing %s tag. '.
+ 'Remove any leading whitespace, and prefer to omit closing tags.',
+ phutil_tag('tt', array(), '<?php'),
+ phutil_tag('tt', array(), '?>'));
}
$this->newIssue('webserver.mangle')
->setName(pht('Mangled Webserver Response'))
->setSummary(pht('Your webserver produced an unexpected response.'))
->setMessage($message);
// We can't run the other checks if we could not decode the response.
- return;
+ if (!$structure) {
+ return;
+ }
}
$actual_user = idx($structure, 'user');
$actual_pass = idx($structure, 'pass');
if (($expect_user != $actual_user) || ($actual_pass != $expect_pass)) {
$message = pht(
'Phabricator sent itself a test request with an "Authorization" HTTP '.
'header, and expected those credentials to be transmitted. However, '.
'they were absent or incorrect when received. Phabricator sent '.
'username "%s" with password "%s"; received username "%s" and '.
'password "%s".'.
"\n\n".
'Your webserver may not be configured to forward HTTP basic '.
'authentication. If you plan to use basic authentication (for '.
'example, to access repositories) you should reconfigure it.',
$expect_user,
$expect_pass,
$actual_user,
$actual_pass);
$this->newIssue('webserver.basic-auth')
->setName(pht('HTTP Basic Auth Not Configured'))
->setSummary(pht('Your webserver is not forwarding credentials.'))
->setMessage($message);
}
$actual_path = idx($structure, 'path');
if ($expect_path != $actual_path) {
$message = pht(
'Phabricator sent itself a test request with an unusual path, to '.
'test if your webserver is rewriting paths correctly. The path was '.
'not transmitted correctly.'.
"\n\n".
'Phabricator sent a request to path "%s", and expected the webserver '.
'to decode and rewrite that path so that it received a request for '.
'"%s". However, it received a request for "%s" instead.'.
"\n\n".
'Verify that your rewrite rules are configured correctly, following '.
'the instructions in the documentation. If path encoding is not '.
'working properly you will be unable to access files with unusual '.
'names in repositories, among other issues.'.
"\n\n".
'(This problem can be caused by a missing "B" in your RewriteRule.)',
$send_path,
$expect_path,
$actual_path);
$this->newIssue('webserver.rewrites')
->setName(pht('HTTP Path Rewriting Incorrect'))
->setSummary(pht('Your webserver is rewriting paths improperly.'))
->setMessage($message);
}
$actual_key = pht('<none>');
$actual_value = pht('<none>');
foreach (idx($structure, 'params', array()) as $pair) {
if (idx($pair, 'name') == $expect_key) {
$actual_key = idx($pair, 'name');
$actual_value = idx($pair, 'value');
break;
}
}
if (($expect_key !== $actual_key) || ($expect_value !== $actual_value)) {
$message = pht(
'Phabricator sent itself a test request with an HTTP GET parameter, '.
'but the parameter was not transmitted. Sent "%s" with value "%s", '.
'got "%s" with value "%s".'.
"\n\n".
'Your webserver is configured incorrectly and large parts of '.
'Phabricator will not work until this issue is corrected.'.
"\n\n".
'(This problem can be caused by a missing "QSA" in your RewriteRule.)',
$expect_key,
$expect_value,
$actual_key,
$actual_value);
$this->newIssue('webserver.parameters')
->setName(pht('HTTP Parameters Not Transmitting'))
->setSummary(
pht('Your webserver is not handling GET parameters properly.'))
->setMessage($message);
}
}
}
diff --git a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php
index ce24d48ea..7e6978dfd 100644
--- a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php
+++ b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php
@@ -1,288 +1,309 @@
<?php
final class PhabricatorMetaMTAConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Mail');
}
public function getDescription() {
return pht('Configure Mail.');
}
public function getIcon() {
return 'fa-send';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
$send_as_user_desc = $this->deformat(pht(<<<EODOC
When a user takes an action which generates an email notification (like
commenting on a Differential revision), Phabricator can either send that mail
"From" the user's email address (like "alincoln@logcabin.com") or "From" the
'%s' address.
The user experience is generally better if Phabricator uses the user's real
address as the "From" since the messages are easier to organize when they appear
in mail clients, but this will only work if the server is authorized to send
email on behalf of the "From" domain. Practically, this means:
- If you are doing an install for Example Corp and all the users will have
corporate @corp.example.com addresses and any hosts Phabricator is running
on are authorized to send email from corp.example.com, you can enable this
to make the user experience a little better.
- If you are doing an install for an open source project and your users will
be registering via Facebook and using personal email addresses, you probably
should not enable this or all of your outgoing email might vanish into SFP
blackholes.
- If your install is anything else, you're safer leaving this off, at least
initially, since the risk in turning it on is that your outgoing mail will
never arrive.
EODOC
,
'metamta.default-address'));
$one_mail_per_recipient_desc = $this->deformat(pht(<<<EODOC
When a message is sent to multiple recipients (for example, several reviewers on
a code review), Phabricator can either deliver one email to everyone (e.g., "To:
alincoln, usgrant, htaft") or separate emails to each user (e.g., "To:
alincoln", "To: usgrant", "To: htaft"). The major advantages and disadvantages
of each approach are:
- One mail to everyone:
- This violates policy controls. The body of the mail is generated without
respect for object policies.
- Recipients can see To/Cc at a glance.
- If you use mailing lists, you won't get duplicate mail if you're
a normal recipient and also Cc'd on a mailing list.
- Getting threading to work properly is harder, and probably requires
making mail less useful by turning off options.
- Sometimes people will "Reply All", which can send mail to too many
recipients. Phabricator will try not to send mail to users who already
received a similar message, but can not prevent all stray email arising
from "Reply All".
- Not supported with a private reply-to address.
- Mail messages are sent in the server default translation.
- Mail that must be delivered over secure channels will leak the recipient
list in the "To" and "Cc" headers.
- One mail to each user:
- Policy controls work correctly and are enforced per-user.
- Recipients need to look in the mail body to see To/Cc.
- If you use mailing lists, recipients may sometimes get duplicate
mail.
- Getting threading to work properly is easier, and threading settings
can be customzied by each user.
- "Reply All" will never send extra mail to other users involved in the
thread.
- Required if private reply-to addresses are configured.
- Mail messages are sent in the language of user preference.
EODOC
));
$reply_hints_description = $this->deformat(pht(<<<EODOC
You can disable the hints under "REPLY HANDLER ACTIONS" if users prefer
smaller messages. The actions themselves will still work properly.
EODOC
));
$recipient_hints_description = $this->deformat(pht(<<<EODOC
You can disable the "To:" and "Cc:" footers in mail if users prefer smaller
messages.
EODOC
));
$email_preferences_description = $this->deformat(pht(<<<EODOC
You can disable the email preference link in emails if users prefer smaller
emails.
EODOC
));
$re_prefix_description = $this->deformat(pht(<<<EODOC
Mail.app on OS X Lion won't respect threading headers unless the subject is
prefixed with "Re:". If you enable this option, Phabricator will add "Re:" to
the subject line of all mail which is expected to thread. If you've set
'metamta.one-mail-per-recipient', users can override this setting in their
preferences.
EODOC
));
$vary_subjects_description = $this->deformat(pht(<<<EODOC
If true, allow MetaMTA to change mail subjects to put text like '[Accepted]' and
'[Commented]' in them. This makes subjects more useful, but might break
threading on some clients. If you've set '%s', users can override this setting
in their preferences.
EODOC
,
'metamta.one-mail-per-recipient'));
$reply_to_description = $this->deformat(pht(<<<EODOC
If you enable `%s`, Phabricator uses "From" to authenticate users. You can
additionally enable this setting to try to authenticate with 'Reply-To'. Note
that this is completely spoofable and insecure (any user can set any 'Reply-To'
address) but depending on the nature of your install or other deliverability
conditions this might be okay. Generally, you can't do much more by spoofing
Reply-To than be annoying (you can write but not read content). But this is
still **COMPLETELY INSECURE**.
EODOC
,
'metamta.public-replies'));
$adapter_description = $this->deformat(pht(<<<EODOC
Adapter class to use to transmit mail to the MTA. The default uses
PHPMailerLite, which will invoke "sendmail". This is appropriate if sendmail
actually works on your host, but if you haven't configured mail it may not be so
great. A number of other mailers are available (e.g., SES, SendGrid, SMTP,
custom mailers). This option is deprecated in favor of 'cluster.mailers'.
EODOC
));
$public_replies_description = $this->deformat(pht(<<<EODOC
By default, Phabricator generates unique reply-to addresses and sends a separate
email to each recipient when you enable reply handling. This is more secure than
using "From" to establish user identity, but can mean users may receive multiple
emails when they are on mailing lists. Instead, you can use a single, non-unique
reply to address and authenticate users based on the "From" address by setting
this to 'true'. This trades away a little bit of security for convenience, but
it's reasonable in many installs. Object interactions are still protected using
hashes in the single public email address, so objects can not be replied to
blindly.
EODOC
));
$single_description = $this->deformat(pht(<<<EODOC
If you want to use a single mailbox for Phabricator reply mail, you can use this
and set a common prefix for reply addresses generated by Phabricator. It will
make use of the fact that a mail-address such as
`phabricator+D123+1hjk213h@example.com` will be delivered to the `phabricator`
user's mailbox. Set this to the left part of the email address and it will be
prepended to all generated reply addresses.
For example, if you want to use `phabricator@example.com`, this should be set
to `phabricator`.
EODOC
));
$address_description = $this->deformat(pht(<<<EODOC
When email is sent, what format should Phabricator use for user's email
addresses? Valid values are:
- `short`: 'gwashington <gwashington@example.com>'
- `real`: 'George Washington <gwashington@example.com>'
- `full`: 'gwashington (George Washington) <gwashington@example.com>'
The default is `full`.
EODOC
));
$mailers_description = $this->deformat(pht(<<<EODOC
Define one or more mail transmission services. For help with configuring
mailers, see **[[ %s | %s ]]** in the documentation.
+EODOC
+ ,
+ PhabricatorEnv::getDoclink('Configuring Outbound Email'),
+ pht('Configuring Outbound Email')));
+
+ $default_description = $this->deformat(pht(<<<EODOC
+Default address used as a "From" or "To" email address when an address is
+required but no meaningful address is available.
+
+If you configure inbound mail, you generally do not need to set this:
+Phabricator will automatically generate and use a suitable mailbox on the
+inbound mail domain.
+
+Otherwise, this option should be configured to point at a valid mailbox which
+discards all mail sent to it. If you point it at an invalid mailbox, mail sent
+by Phabricator and some mail sent by users will bounce. If you point it at a
+real user mailbox, that user will get a lot of mail they don't want.
+
+For further guidance, see **[[ %s | %s ]]** in the documentation.
EODOC
,
PhabricatorEnv::getDoclink('Configuring Outbound Email'),
pht('Configuring Outbound Email')));
return array(
$this->newOption('cluster.mailers', 'cluster.mailers', array())
->setHidden(true)
->setDescription($mailers_description),
$this->newOption('metamta.default-address', 'string', null)
- ->setDescription(pht('Default "From" address.')),
+ ->setLocked(true)
+ ->setSummary(pht('Default address used when generating mail.'))
+ ->setDescription($default_description),
$this->newOption(
'metamta.one-mail-per-recipient',
'bool',
true)
->setLocked(true)
->setBoolOptions(
array(
pht('Send Mail To Each Recipient'),
pht('Send Mail To All Recipients'),
))
->setSummary(
pht(
'Controls whether Phabricator sends one email with multiple '.
'recipients in the "To:" line, or multiple emails, each with a '.
'single recipient in the "To:" line.'))
->setDescription($one_mail_per_recipient_desc),
$this->newOption('metamta.can-send-as-user', 'bool', false)
->setBoolOptions(
array(
pht('Send as User Taking Action'),
pht('Send as Phabricator'),
))
->setSummary(
pht(
'Controls whether Phabricator sends email "From" users.'))
->setDescription($send_as_user_desc),
$this->newOption(
'metamta.reply-handler-domain',
'string',
null)
->setLocked(true)
->setDescription(pht('Domain used for reply email addresses.'))
->addExample('phabricator.example.com', ''),
$this->newOption('metamta.recipients.show-hints', 'bool', true)
->setBoolOptions(
array(
pht('Show Recipient Hints'),
pht('No Recipient Hints'),
))
->setSummary(pht('Show "To:" and "Cc:" footer hints in email.'))
->setDescription($recipient_hints_description),
$this->newOption('metamta.email-preferences', 'bool', true)
->setBoolOptions(
array(
pht('Show Email Preferences Link'),
pht('No Email Preferences Link'),
))
->setSummary(pht('Show email preferences link in email.'))
->setDescription($email_preferences_description),
$this->newOption('metamta.public-replies', 'bool', false)
->setBoolOptions(
array(
pht('Use Public Replies (Less Secure)'),
pht('Use Private Replies (More Secure)'),
))
->setSummary(
pht(
'Phabricator can use less-secure but mailing list friendly public '.
'reply addresses.'))
->setDescription($public_replies_description),
$this->newOption('metamta.single-reply-handler-prefix', 'string', null)
->setSummary(
pht('Allow Phabricator to use a single mailbox for all replies.'))
->setDescription($single_description),
$this->newOption('metamta.user-address-format', 'enum', 'full')
->setEnumOptions(
array(
'short' => pht('Short'),
'real' => pht('Real'),
'full' => pht('Full'),
))
->setSummary(pht('Control how Phabricator renders user names in mail.'))
->setDescription($address_description)
->addExample('gwashington <gwashington@example.com>', 'short')
->addExample('George Washington <gwashington@example.com>', 'real')
->addExample(
'gwashington (George Washington) <gwashington@example.com>',
'full'),
$this->newOption('metamta.email-body-limit', 'int', 524288)
->setDescription(
pht(
'You can set a limit for the maximum byte size of outbound mail. '.
'Mail which is larger than this limit will be truncated before '.
'being sent. This can be useful if your MTA rejects mail which '.
'exceeds some limit (this is reasonably common). Specify a value '.
'in bytes.'))
->setSummary(pht('Global cap for size of generated emails (bytes).'))
->addExample(524288, pht('Truncate at 512KB'))
->addExample(1048576, pht('Truncate at 1MB')),
);
}
}
diff --git a/src/applications/config/option/PhabricatorPHDConfigOptions.php b/src/applications/config/option/PhabricatorPHDConfigOptions.php
index 37fae45df..e04353876 100644
--- a/src/applications/config/option/PhabricatorPHDConfigOptions.php
+++ b/src/applications/config/option/PhabricatorPHDConfigOptions.php
@@ -1,100 +1,105 @@
<?php
final class PhabricatorPHDConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Daemons');
}
public function getDescription() {
return pht('Options relating to PHD (daemons).');
}
public function getIcon() {
return 'fa-pied-piper-alt';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
return array(
$this->newOption('phd.pid-directory', 'string', '/var/tmp/phd/pid')
->setLocked(true)
->setDescription(
pht('Directory that phd should use to track running daemons.')),
$this->newOption('phd.log-directory', 'string', '/var/tmp/phd/log')
->setLocked(true)
->setDescription(
pht('Directory that the daemons should use to store log files.')),
$this->newOption('phd.taskmasters', 'int', 4)
->setLocked(true)
->setSummary(pht('Maximum taskmaster daemon pool size.'))
->setDescription(
pht(
"Maximum number of taskmaster daemons to run at once. Raising ".
"this can increase the maximum throughput of the task queue. The ".
"pool will automatically scale down when unutilized.".
"\n\n".
"If you are running a cluster, this limit applies separately ".
"to each instance of `phd`. For example, if this limit is set ".
"to `4` and you have three hosts running daemons, the effective ".
- "global limit will be 12.")),
+ "global limit will be 12.".
+ "\n\n".
+ "After changing this value, you must restart the daemons. Most ".
+ "configuration changes are picked up by the daemons ".
+ "automatically, but pool sizes can not be changed without a ".
+ "restart.")),
$this->newOption('phd.verbose', 'bool', false)
->setLocked(true)
->setBoolOptions(
array(
pht('Verbose mode'),
pht('Normal mode'),
))
->setSummary(pht("Launch daemons in 'verbose' mode by default."))
->setDescription(
pht(
"Launch daemons in 'verbose' mode by default. This creates a lot ".
"of output, but can help debug issues. Daemons launched in debug ".
"mode with '%s' are always launched in verbose mode. ".
"See also '%s'.",
'phd debug',
'phd.trace')),
$this->newOption('phd.user', 'string', null)
->setLocked(true)
->setSummary(pht('System user to run daemons as.'))
->setDescription(
pht(
'Specify a system user to run the daemons as. Primarily, this '.
'user will own the working copies of any repositories that '.
'Phabricator imports or manages. This option is new and '.
'experimental.')),
$this->newOption('phd.trace', 'bool', false)
->setLocked(true)
->setBoolOptions(
array(
pht('Trace mode'),
pht('Normal mode'),
))
->setSummary(pht("Launch daemons in 'trace' mode by default."))
->setDescription(
pht(
"Launch daemons in 'trace' mode by default. This creates an ".
"ENORMOUS amount of output, but can help debug issues. Daemons ".
"launched in debug mode with '%s' are always launched in ".
"trace mode. See also '%s'.",
'phd debug',
'phd.verbose')),
$this->newOption('phd.garbage-collection', 'wild', array())
->setLocked(true)
->setLockedMessage(
pht(
'This option can not be edited from the web UI. Use %s to adjust '.
'garbage collector policies.',
phutil_tag('tt', array(), 'bin/garbage set-policy')))
->setSummary(pht('Retention policies for garbage collection.'))
->setDescription(
pht(
'Customizes retention policies for garbage collectors.')),
);
}
}
diff --git a/src/applications/config/storage/PhabricatorConfigTransaction.php b/src/applications/config/storage/PhabricatorConfigTransaction.php
index b7cfb6f49..94272bfb1 100644
--- a/src/applications/config/storage/PhabricatorConfigTransaction.php
+++ b/src/applications/config/storage/PhabricatorConfigTransaction.php
@@ -1,156 +1,152 @@
<?php
final class PhabricatorConfigTransaction
extends PhabricatorApplicationTransaction {
const TYPE_EDIT = 'config:edit';
public function getApplicationName() {
return 'config';
}
public function getApplicationTransactionType() {
return PhabricatorConfigConfigPHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return null;
- }
-
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_EDIT:
// TODO: After T2213 show the actual values too; for now, we don't
// have the tools to do it without making a bit of a mess of it.
$old_del = idx($old, 'deleted');
$new_del = idx($new, 'deleted');
if ($old_del && !$new_del) {
return pht(
'%s created this configuration entry.',
$this->renderHandleLink($author_phid));
} else if (!$old_del && $new_del) {
return pht(
'%s deleted this configuration entry.',
$this->renderHandleLink($author_phid));
} else if ($old_del && $new_del) {
// This is a bug.
return pht(
'%s deleted this configuration entry (again?).',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s edited this configuration entry.',
$this->renderHandleLink($author_phid));
}
break;
}
return parent::getTitle();
}
public function getTitleForFeed() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_EDIT:
$old_del = idx($old, 'deleted');
$new_del = idx($new, 'deleted');
if ($old_del && !$new_del) {
return pht(
'%s created %s.',
$this->renderHandleLink($author_phid),
$this->getObject()->getConfigKey());
} else if (!$old_del && $new_del) {
return pht(
'%s deleted %s.',
$this->renderHandleLink($author_phid),
$this->getObject()->getConfigKey());
} else if ($old_del && $new_del) {
// This is a bug.
return pht(
'%s deleted %s (again?).',
$this->renderHandleLink($author_phid),
$this->getObject()->getConfigKey());
} else {
return pht(
'%s edited %s.',
$this->renderHandleLink($author_phid),
$this->getObject()->getConfigKey());
}
break;
}
return parent::getTitle();
}
public function getIcon() {
switch ($this->getTransactionType()) {
case self::TYPE_EDIT:
return 'fa-pencil';
}
return parent::getIcon();
}
public function hasChangeDetails() {
switch ($this->getTransactionType()) {
case self::TYPE_EDIT:
return true;
}
return parent::hasChangeDetails();
}
public function renderChangeDetails(PhabricatorUser $viewer) {
$old = $this->getOldValue();
$new = $this->getNewValue();
if ($old['deleted']) {
$old_text = '';
} else {
$old_text = PhabricatorConfigJSON::prettyPrintJSON($old['value']);
}
if ($new['deleted']) {
$new_text = '';
} else {
$new_text = PhabricatorConfigJSON::prettyPrintJSON($new['value']);
}
return $this->renderTextCorpusChangeDetails(
$viewer,
$old_text,
$new_text);
}
public function getColor() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_EDIT:
$old_del = idx($old, 'deleted');
$new_del = idx($new, 'deleted');
if ($old_del && !$new_del) {
return PhabricatorTransactions::COLOR_GREEN;
} else if (!$old_del && $new_del) {
return PhabricatorTransactions::COLOR_RED;
} else {
return PhabricatorTransactions::COLOR_BLUE;
}
break;
}
}
}
diff --git a/src/applications/config/type/PhabricatorSetConfigType.php b/src/applications/config/type/PhabricatorSetConfigType.php
index 805ae5046..553ee614b 100644
--- a/src/applications/config/type/PhabricatorSetConfigType.php
+++ b/src/applications/config/type/PhabricatorSetConfigType.php
@@ -1,92 +1,92 @@
<?php
final class PhabricatorSetConfigType
extends PhabricatorTextConfigType {
const TYPEKEY = 'set';
protected function newControl(PhabricatorConfigOption $option) {
return id(new AphrontFormTextAreaControl())
->setCaption(pht('Separate values with newlines or commas.'));
}
protected function newCanonicalValue(
PhabricatorConfigOption $option,
$value) {
$value = preg_split('/[\n,]+/', $value);
foreach ($value as $k => $v) {
if (!strlen($v)) {
unset($value[$k]);
}
$value[$k] = trim($v);
}
return array_fill_keys($value, true);
}
public function newValueFromCommandLineValue(
PhabricatorConfigOption $option,
$value) {
try {
$value = phutil_json_decode($value);
} catch (Exception $ex) {
throw $this->newException(
pht(
'Option "%s" is of type "%s", but the value you provided is not a '.
'valid JSON list: when providing a set from the command line, '.
'specify it as a list of values in JSON. You may need to quote the '.
'value for your shell (for example: \'["a", "b", ...]\').',
$option->getKey(),
$this->getTypeKey()));
}
if ($value) {
- if (array_keys($value) !== range(0, count($value) - 1)) {
+ if (!phutil_is_natural_list($value)) {
throw $this->newException(
pht(
'Option "%s" is of type "%s", and should be specified on the '.
'command line as a JSON list of values. You may need to quote '.
'the value for your shell (for example: \'["a", "b", ...]\').',
$option->getKey(),
$this->getTypeKey()));
}
}
return array_fill_keys($value, true);
}
public function newDisplayValue(
PhabricatorConfigOption $option,
$value) {
return implode("\n", array_keys($value));
}
public function validateStoredValue(
PhabricatorConfigOption $option,
$value) {
if (!is_array($value)) {
throw $this->newException(
pht(
'Option "%s" is of type "%s", but the configured value is not '.
'a list.',
$option->getKey(),
$this->getTypeKey()));
}
foreach ($value as $k => $v) {
if ($v !== true) {
throw $this->newException(
pht(
'Option "%s" is of type "%s", but the value at index "%s" of the '.
'list is not "true".',
$option->getKey(),
$this->getTypeKey(),
$k));
}
}
}
}
diff --git a/src/applications/conpherence/controller/ConpherenceViewController.php b/src/applications/conpherence/controller/ConpherenceViewController.php
index 996417a30..357d07631 100644
--- a/src/applications/conpherence/controller/ConpherenceViewController.php
+++ b/src/applications/conpherence/controller/ConpherenceViewController.php
@@ -1,211 +1,211 @@
<?php
final class ConpherenceViewController extends
ConpherenceController {
const OLDER_FETCH_LIMIT = 5;
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$user = $request->getUser();
$conpherence_id = $request->getURIData('id');
if (!$conpherence_id) {
return new Aphront404Response();
}
$query = id(new ConpherenceThreadQuery())
->setViewer($user)
->withIDs(array($conpherence_id))
->needProfileImage(true)
->needTransactions(true)
->setTransactionLimit($this->getMainQueryLimit());
$before_transaction_id = $request->getInt('oldest_transaction_id');
$after_transaction_id = $request->getInt('newest_transaction_id');
$old_message_id = $request->getURIData('messageID');
if ($before_transaction_id && ($old_message_id || $after_transaction_id)) {
throw new Aphront400Response();
}
if ($old_message_id && $after_transaction_id) {
throw new Aphront400Response();
}
$marker_type = 'older';
if ($before_transaction_id) {
$query
->setBeforeTransactionID($before_transaction_id);
}
if ($old_message_id) {
$marker_type = 'olderandnewer';
$query
->setAfterTransactionID($old_message_id - 1);
}
if ($after_transaction_id) {
$marker_type = 'newer';
$query
->setAfterTransactionID($after_transaction_id);
}
$conpherence = $query->executeOne();
if (!$conpherence) {
return new Aphront404Response();
}
$this->setConpherence($conpherence);
$participant = $conpherence->getParticipantIfExists($user->getPHID());
$theme = ConpherenceRoomSettings::COLOR_LIGHT;
if ($participant) {
$settings = $participant->getSettings();
$theme = idx($settings, 'theme', ConpherenceRoomSettings::COLOR_LIGHT);
if (!$participant->isUpToDate($conpherence)) {
$write_guard = AphrontWriteGuard::beginScopedUnguardedWrites();
$participant->markUpToDate($conpherence);
$user->clearCacheData(PhabricatorUserMessageCountCacheType::KEY_COUNT);
unset($write_guard);
}
}
$data = ConpherenceTransactionRenderer::renderTransactions(
$user,
$conpherence,
$marker_type);
$messages = ConpherenceTransactionRenderer::renderMessagePaneContent(
$data['transactions'],
$data['oldest_transaction_id'],
$data['newest_transaction_id']);
if ($before_transaction_id || $after_transaction_id) {
$header = null;
$form = null;
$content = array('transactions' => $messages);
} else {
$header = $this->buildHeaderPaneContent($conpherence);
$search = $this->buildSearchForm();
$form = $this->renderFormContent();
$content = array(
'header' => $header,
'search' => $search,
'transactions' => $messages,
'form' => $form,
);
}
$d_data = $conpherence->getDisplayData($user);
$content['title'] = $title = $d_data['title'];
if ($request->isAjax()) {
$dropdown_query = id(new AphlictDropdownDataQuery())
->setViewer($user);
$dropdown_query->execute();
$content['threadID'] = $conpherence->getID();
$content['threadPHID'] = $conpherence->getPHID();
$content['latestTransactionID'] = $data['latest_transaction_id'];
$content['canEdit'] = PhabricatorPolicyFilter::hasCapability(
$user,
$conpherence,
PhabricatorPolicyCapability::CAN_EDIT);
$content['aphlictDropdownData'] = array(
$dropdown_query->getNotificationData(),
$dropdown_query->getConpherenceData(),
);
return id(new AphrontAjaxResponse())->setContent($content);
}
$layout = id(new ConpherenceLayoutView())
->setUser($user)
->setBaseURI($this->getApplicationURI())
->setThread($conpherence)
->setHeader($header)
->setSearch($search)
->setMessages($messages)
->setReplyForm($form)
->setTheme($theme)
->setLatestTransactionID($data['latest_transaction_id'])
->setRole('thread');
$participating = $conpherence->getParticipantIfExists($user->getPHID());
if (!$user->isLoggedIn()) {
$layout->addClass('conpherence-no-pontificate');
}
return $this->newPage()
->setTitle($title)
->setPageObjectPHIDs(array($conpherence->getPHID()))
->appendChild($layout);
}
private function renderFormContent() {
$conpherence = $this->getConpherence();
$user = $this->getRequest()->getUser();
$participating = $conpherence->getParticipantIfExists($user->getPHID());
$draft = PhabricatorDraft::newFromUserAndKey(
$user,
$conpherence->getPHID());
$update_uri = $this->getApplicationURI('update/'.$conpherence->getID().'/');
if ($user->isLoggedIn()) {
$this->initBehavior('conpherence-pontificate');
if ($participating) {
$action = ConpherenceUpdateActions::MESSAGE;
$status = new PhabricatorNotificationStatusView();
} else {
$action = ConpherenceUpdateActions::JOIN_ROOM;
$status = pht('Sending a message will also join the room.');
}
$form = id(new AphrontFormView())
->setUser($user)
->setAction($update_uri)
->addSigil('conpherence-pontificate')
->setWorkflow(true)
->addHiddenInput('action', $action)
->appendChild(
id(new PhabricatorRemarkupControl())
->setUser($user)
->setName('text')
->setSendOnEnter(true)
->setValue($draft->getDraft()));
$status_view = phutil_tag(
'div',
array(
'class' => 'conpherence-room-status',
'id' => 'conpherence-room-status',
),
$status);
$view = phutil_tag_div(
'pontificate-container', array($form, $status_view));
return $view;
} else {
// user not logged in so give them a login button.
$login_href = id(new PhutilURI('/auth/start/'))
- ->setQueryParam('next', '/'.$conpherence->getMonogram());
+ ->replaceQueryParam('next', '/'.$conpherence->getMonogram());
return id(new PHUIFormLayoutView())
->addClass('login-to-participate')
->appendInstructions(pht('Log in to join this room and participate.'))
->appendChild(
id(new PHUIButtonView())
->setTag('a')
->setText(pht('Log In to Participate'))
->setHref((string)$login_href));
}
}
private function getMainQueryLimit() {
$request = $this->getRequest();
$base_limit = ConpherenceThreadQuery::TRANSACTION_LIMIT;
if ($request->getURIData('messageID')) {
$base_limit = $base_limit - self::OLDER_FETCH_LIMIT;
}
return $base_limit;
}
}
diff --git a/src/applications/conpherence/engineextension/ConpherenceThreadIndexEngineExtension.php b/src/applications/conpherence/engineextension/ConpherenceThreadIndexEngineExtension.php
index d45e34772..740a1d81e 100644
--- a/src/applications/conpherence/engineextension/ConpherenceThreadIndexEngineExtension.php
+++ b/src/applications/conpherence/engineextension/ConpherenceThreadIndexEngineExtension.php
@@ -1,81 +1,84 @@
<?php
final class ConpherenceThreadIndexEngineExtension
extends PhabricatorIndexEngineExtension {
const EXTENSIONKEY = 'conpherence.thread';
public function getExtensionName() {
return pht('Conpherence Threads');
}
public function shouldIndexObject($object) {
return ($object instanceof ConpherenceThread);
}
public function indexObject(
PhabricatorIndexEngine $engine,
$object) {
$force = $this->shouldForceFullReindex();
if (!$force) {
$xaction_phids = $this->getParameter('transactionPHIDs');
if (!$xaction_phids) {
return;
}
}
$query = id(new ConpherenceTransactionQuery())
->setViewer($this->getViewer())
->withObjectPHIDs(array($object->getPHID()))
->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT))
->needComments(true);
if (!$force) {
$query->withPHIDs($xaction_phids);
}
$xactions = $query->execute();
if (!$xactions) {
return;
}
foreach ($xactions as $xaction) {
$this->indexComment($object, $xaction);
}
}
private function indexComment(
ConpherenceThread $thread,
ConpherenceTransaction $xaction) {
- $previous = id(new ConpherenceTransactionQuery())
+ $pager = id(new AphrontCursorPagerView())
+ ->setPageSize(1)
+ ->setAfterID($xaction->getID());
+
+ $previous_xactions = id(new ConpherenceTransactionQuery())
->setViewer($this->getViewer())
->withObjectPHIDs(array($thread->getPHID()))
->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT))
- ->setAfterID($xaction->getID())
- ->setLimit(1)
- ->executeOne();
+ ->executeWithCursorPager($pager);
+ $previous = head($previous_xactions);
$index = id(new ConpherenceIndex())
->setThreadPHID($thread->getPHID())
->setTransactionPHID($xaction->getPHID())
->setPreviousTransactionPHID($previous ? $previous->getPHID() : null)
->setCorpus($xaction->getComment()->getContent());
queryfx(
$index->establishConnection('w'),
'INSERT INTO %T
(threadPHID, transactionPHID, previousTransactionPHID, corpus)
VALUES (%s, %s, %ns, %s)
ON DUPLICATE KEY UPDATE corpus = VALUES(corpus)',
$index->getTableName(),
$index->getThreadPHID(),
$index->getTransactionPHID(),
$index->getPreviousTransactionPHID(),
$index->getCorpus());
}
}
diff --git a/src/applications/conpherence/query/ConpherenceThreadQuery.php b/src/applications/conpherence/query/ConpherenceThreadQuery.php
index 5cd6489d6..9c6682a8a 100644
--- a/src/applications/conpherence/query/ConpherenceThreadQuery.php
+++ b/src/applications/conpherence/query/ConpherenceThreadQuery.php
@@ -1,326 +1,338 @@
<?php
final class ConpherenceThreadQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
const TRANSACTION_LIMIT = 100;
private $phids;
private $ids;
private $participantPHIDs;
private $needParticipants;
private $needTransactions;
private $afterTransactionID;
private $beforeTransactionID;
private $transactionLimit;
private $fulltext;
private $needProfileImage;
public function needParticipants($need) {
$this->needParticipants = $need;
return $this;
}
public function needProfileImage($need) {
$this->needProfileImage = $need;
return $this;
}
public function needTransactions($need_transactions) {
$this->needTransactions = $need_transactions;
return $this;
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withParticipantPHIDs(array $phids) {
$this->participantPHIDs = $phids;
return $this;
}
public function setAfterTransactionID($id) {
$this->afterTransactionID = $id;
return $this;
}
public function setBeforeTransactionID($id) {
$this->beforeTransactionID = $id;
return $this;
}
public function setTransactionLimit($transaction_limit) {
$this->transactionLimit = $transaction_limit;
return $this;
}
public function getTransactionLimit() {
return $this->transactionLimit;
}
public function withFulltext($query) {
$this->fulltext = $query;
return $this;
}
public function withTitleNgrams($ngrams) {
return $this->withNgramsConstraint(
id(new ConpherenceThreadTitleNgrams()),
$ngrams);
}
protected function loadPage() {
$table = new ConpherenceThread();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT thread.* FROM %T thread %Q %Q %Q %Q %Q',
$table->getTableName(),
$this->buildJoinClause($conn_r),
$this->buildWhereClause($conn_r),
$this->buildGroupClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
$conpherences = $table->loadAllFromArray($data);
if ($conpherences) {
$conpherences = mpull($conpherences, null, 'getPHID');
$this->loadParticipantsAndInitHandles($conpherences);
if ($this->needParticipants) {
$this->loadCoreHandles($conpherences, 'getParticipantPHIDs');
}
if ($this->needTransactions) {
$this->loadTransactionsAndHandles($conpherences);
}
if ($this->needProfileImage) {
$default = null;
$file_phids = mpull($conpherences, 'getProfileImagePHID');
$file_phids = array_filter($file_phids);
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
} else {
$files = array();
}
foreach ($conpherences as $conpherence) {
$file = idx($files, $conpherence->getProfileImagePHID());
if (!$file) {
if (!$default) {
$default = PhabricatorFile::loadBuiltin(
$this->getViewer(),
'conpherence.png');
}
$file = $default;
}
$conpherence->attachProfileImageFile($file);
}
}
}
return $conpherences;
}
protected function buildGroupClause(AphrontDatabaseConnection $conn_r) {
if ($this->participantPHIDs !== null || strlen($this->fulltext)) {
return 'GROUP BY thread.id';
} else {
return $this->buildApplicationSearchGroupClause($conn_r);
}
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->participantPHIDs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T p ON p.conpherencePHID = thread.phid',
id(new ConpherenceParticipant())->getTableName());
}
if (strlen($this->fulltext)) {
$joins[] = qsprintf(
$conn,
'JOIN %T idx ON idx.threadPHID = thread.phid',
id(new ConpherenceIndex())->getTableName());
}
// See note in buildWhereClauseParts() about this optimization.
$viewer = $this->getViewer();
if (!$viewer->isOmnipotent() && $viewer->isLoggedIn()) {
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T vp ON vp.conpherencePHID = thread.phid
AND vp.participantPHID = %s',
id(new ConpherenceParticipant())->getTableName(),
$viewer->getPHID());
}
return $joins;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
// Optimize policy filtering of private rooms. If we are not looking for
// particular rooms by ID or PHID, we can just skip over any rooms with
// "View Policy: Room Participants" if the viewer isn't a participant: we
// know they won't be able to see the room.
// This avoids overheating browse/search queries, since it's common for
// a large number of rooms to be private and have this view policy.
$viewer = $this->getViewer();
$can_optimize =
!$viewer->isOmnipotent() &&
($this->ids === null) &&
($this->phids === null);
if ($can_optimize) {
$members_policy = id(new ConpherenceThreadMembersPolicyRule())
->getObjectPolicyFullKey();
if ($viewer->isLoggedIn()) {
$where[] = qsprintf(
$conn,
'thread.viewPolicy != %s OR vp.participantPHID = %s',
$members_policy,
$viewer->getPHID());
} else {
$where[] = qsprintf(
$conn,
'thread.viewPolicy != %s',
$members_policy);
}
}
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'thread.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'thread.phid IN (%Ls)',
$this->phids);
}
if ($this->participantPHIDs !== null) {
$where[] = qsprintf(
$conn,
'p.participantPHID IN (%Ls)',
$this->participantPHIDs);
}
if (strlen($this->fulltext)) {
$where[] = qsprintf(
$conn,
'MATCH(idx.corpus) AGAINST (%s IN BOOLEAN MODE)',
$this->fulltext);
}
return $where;
}
private function loadParticipantsAndInitHandles(array $conpherences) {
$participants = id(new ConpherenceParticipant())
->loadAllWhere('conpherencePHID IN (%Ls)', array_keys($conpherences));
$map = mgroup($participants, 'getConpherencePHID');
foreach ($conpherences as $current_conpherence) {
$conpherence_phid = $current_conpherence->getPHID();
$conpherence_participants = idx(
$map,
$conpherence_phid,
array());
$conpherence_participants = mpull(
$conpherence_participants,
null,
'getParticipantPHID');
$current_conpherence->attachParticipants($conpherence_participants);
$current_conpherence->attachHandles(array());
}
return $this;
}
private function loadCoreHandles(
array $conpherences,
$method) {
$handle_phids = array();
foreach ($conpherences as $conpherence) {
$handle_phids[$conpherence->getPHID()] =
$conpherence->$method();
}
$flat_phids = array_mergev($handle_phids);
$viewer = $this->getViewer();
$handles = $viewer->loadHandles($flat_phids);
$handles = iterator_to_array($handles);
foreach ($handle_phids as $conpherence_phid => $phids) {
$conpherence = $conpherences[$conpherence_phid];
$conpherence->attachHandles(
$conpherence->getHandles() + array_select_keys($handles, $phids));
}
return $this;
}
private function loadTransactionsAndHandles(array $conpherences) {
- $query = id(new ConpherenceTransactionQuery())
- ->setViewer($this->getViewer())
- ->withObjectPHIDs(array_keys($conpherences))
- ->needHandles(true);
+ // NOTE: This is older code which has been modernized to the minimum
+ // standard required by T13266. It probably isn't the best available
+ // approach to the problems it solves.
+
+ $limit = $this->getTransactionLimit();
+ if ($limit) {
+ // fetch an extra for "show older" scenarios
+ $limit = $limit + 1;
+ } else {
+ $limit = 0xFFFF;
+ }
+
+ $pager = id(new AphrontCursorPagerView())
+ ->setPageSize($limit);
// We have to flip these for the underlying query class. The semantics of
// paging are tricky business.
if ($this->afterTransactionID) {
- $query->setBeforeID($this->afterTransactionID);
+ $pager->setBeforeID($this->afterTransactionID);
} else if ($this->beforeTransactionID) {
- $query->setAfterID($this->beforeTransactionID);
+ $pager->setAfterID($this->beforeTransactionID);
}
- if ($this->getTransactionLimit()) {
- // fetch an extra for "show older" scenarios
- $query->setLimit($this->getTransactionLimit() + 1);
- }
- $transactions = $query->execute();
+
+ $transactions = id(new ConpherenceTransactionQuery())
+ ->setViewer($this->getViewer())
+ ->withObjectPHIDs(array_keys($conpherences))
+ ->needHandles(true)
+ ->executeWithCursorPager($pager);
+
$transactions = mgroup($transactions, 'getObjectPHID');
foreach ($conpherences as $phid => $conpherence) {
$current_transactions = idx($transactions, $phid, array());
$handles = array();
foreach ($current_transactions as $transaction) {
$handles += $transaction->getHandles();
}
$conpherence->attachHandles($conpherence->getHandles() + $handles);
$conpherence->attachTransactions($current_transactions);
}
return $this;
}
public function getQueryApplicationClass() {
return 'PhabricatorConpherenceApplication';
}
protected function getPrimaryTableAlias() {
return 'thread';
}
}
diff --git a/src/applications/countdown/query/PhabricatorCountdownQuery.php b/src/applications/countdown/query/PhabricatorCountdownQuery.php
index e6c410ee4..67a2f3a9e 100644
--- a/src/applications/countdown/query/PhabricatorCountdownQuery.php
+++ b/src/applications/countdown/query/PhabricatorCountdownQuery.php
@@ -1,108 +1,107 @@
<?php
final class PhabricatorCountdownQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $authorPHIDs;
private $upcoming;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAuthorPHIDs(array $author_phids) {
$this->authorPHIDs = $author_phids;
return $this;
}
public function withUpcoming() {
$this->upcoming = true;
return $this;
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
public function newResultObject() {
return new PhabricatorCountdown();
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->authorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'authorPHID in (%Ls)',
$this->authorPHIDs);
}
if ($this->upcoming !== null) {
$where[] = qsprintf(
$conn,
'epoch >= %d',
PhabricatorTime::getNow());
}
return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorCountdownApplication';
}
public function getBuiltinOrders() {
return array(
'ending' => array(
'vector' => array('-epoch', '-id'),
'name' => pht('End Date (Past to Future)'),
),
'unending' => array(
'vector' => array('epoch', 'id'),
'name' => pht('End Date (Future to Past)'),
),
) + parent::getBuiltinOrders();
}
public function getOrderableColumns() {
return array(
'epoch' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'epoch',
'type' => 'int',
),
) + parent::getOrderableColumns();
}
- protected function getPagingValueMap($cursor, array $keys) {
- $countdown = $this->loadCursorObject($cursor);
+ protected function newPagingMapFromPartialObject($object) {
return array(
- 'epoch' => $countdown->getEpoch(),
- 'id' => $countdown->getID(),
+ 'id' => (int)$object->getID(),
+ 'epoch' => (int)$object->getEpoch(),
);
}
}
diff --git a/src/applications/daemon/application/PhabricatorDaemonsApplication.php b/src/applications/daemon/application/PhabricatorDaemonsApplication.php
index a0fb77beb..08e81d5d7 100644
--- a/src/applications/daemon/application/PhabricatorDaemonsApplication.php
+++ b/src/applications/daemon/application/PhabricatorDaemonsApplication.php
@@ -1,62 +1,61 @@
<?php
final class PhabricatorDaemonsApplication extends PhabricatorApplication {
public function getName() {
return pht('Daemons');
}
public function getShortDescription() {
return pht('Manage Phabricator Daemons');
}
public function getBaseURI() {
return '/daemon/';
}
public function getTitleGlyph() {
return "\xE2\x98\xAF";
}
public function getIcon() {
return 'fa-pied-piper-alt';
}
public function getApplicationGroup() {
return self::GROUP_ADMIN;
}
public function canUninstall() {
return false;
}
public function getEventListeners() {
return array(
new PhabricatorDaemonEventListener(),
);
}
public function getRoutes() {
return array(
'/daemon/' => array(
'' => 'PhabricatorDaemonConsoleController',
'task/(?P<id>[1-9]\d*)/' => 'PhabricatorWorkerTaskDetailController',
'log/' => array(
'' => 'PhabricatorDaemonLogListController',
'(?P<id>[1-9]\d*)/' => 'PhabricatorDaemonLogViewController',
),
- 'event/(?P<id>[1-9]\d*)/' => 'PhabricatorDaemonLogEventViewController',
'bulk/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?' =>
'PhabricatorDaemonBulkJobListController',
'monitor/(?P<id>\d+)/' =>
'PhabricatorDaemonBulkJobMonitorController',
'view/(?P<id>\d+)/' =>
'PhabricatorDaemonBulkJobViewController',
),
),
);
}
}
diff --git a/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php b/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php
index a70cde04c..421008082 100644
--- a/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php
+++ b/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php
@@ -1,269 +1,290 @@
<?php
final class PhabricatorDaemonConsoleController
extends PhabricatorDaemonController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$window_start = (time() - (60 * 15));
// Assume daemons spend about 250ms second in overhead per task acquiring
// leases and doing other bookkeeping. This is probably an over-estimation,
// but we'd rather show that utilization is too high than too low.
$lease_overhead = 0.250;
$completed = id(new PhabricatorWorkerArchiveTaskQuery())
->withDateModifiedSince($window_start)
->execute();
$failed = id(new PhabricatorWorkerActiveTask())->loadAllWhere(
'failureTime > %d',
$window_start);
$usage_total = 0;
$usage_start = PHP_INT_MAX;
$completed_info = array();
foreach ($completed as $completed_task) {
$class = $completed_task->getTaskClass();
if (empty($completed_info[$class])) {
$completed_info[$class] = array(
'n' => 0,
'duration' => 0,
+ 'queueTime' => 0,
);
}
$completed_info[$class]['n']++;
$duration = $completed_task->getDuration();
$completed_info[$class]['duration'] += $duration;
// NOTE: Duration is in microseconds, but we're just using seconds to
// compute utilization.
$usage_total += $lease_overhead + ($duration / 1000000);
$usage_start = min($usage_start, $completed_task->getDateModified());
+
+ $date_archived = $completed_task->getArchivedEpoch();
+ $queue_seconds = $date_archived - $completed_task->getDateCreated();
+
+ // Don't measure queue time for tasks that completed in the same
+ // epoch-second they were created in.
+ if ($queue_seconds > 0) {
+ $sec_in_us = phutil_units('1 second in microseconds');
+ $queue_us = $queue_seconds * $sec_in_us;
+ $queue_exclusive_us = $queue_us - $duration;
+ $queue_exclusive_seconds = $queue_exclusive_us / $sec_in_us;
+ $rounded = floor($queue_exclusive_seconds);
+ $completed_info[$class]['queueTime'] += $rounded;
+ }
}
$completed_info = isort($completed_info, 'n');
$rows = array();
foreach ($completed_info as $class => $info) {
+ $duration_avg = new PhutilNumber((int)($info['duration'] / $info['n']));
+ $queue_avg = new PhutilNumber((int)($info['queueTime'] / $info['n']));
$rows[] = array(
$class,
number_format($info['n']),
- pht('%s us', new PhutilNumber((int)($info['duration'] / $info['n']))),
+ pht('%s us', $duration_avg),
+ pht('%s s', $queue_avg),
);
}
if ($failed) {
// Add the time it takes to restart the daemons. This includes a guess
// about other overhead of 2X.
$restart_delay = PhutilDaemonHandle::getWaitBeforeRestart();
$usage_total += $restart_delay * count($failed) * 2;
foreach ($failed as $failed_task) {
$usage_start = min($usage_start, $failed_task->getFailureTime());
}
$rows[] = array(
phutil_tag('em', array(), pht('Temporary Failures')),
count($failed),
null,
);
}
$logs = id(new PhabricatorDaemonLogQuery())
->setViewer($viewer)
->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE)
->setAllowStatusWrites(true)
->execute();
$taskmasters = 0;
foreach ($logs as $log) {
if ($log->getDaemon() == 'PhabricatorTaskmasterDaemon') {
$taskmasters++;
}
}
if ($taskmasters && $usage_total) {
// Total number of wall-time seconds the daemons have been running since
// the oldest event. For very short times round up to 15s so we don't
// render any ridiculous numbers if you reload the page immediately after
// restarting the daemons.
$available_time = $taskmasters * max(15, (time() - $usage_start));
// Percentage of those wall-time seconds we can account for, which the
// daemons spent doing work:
$used_time = ($usage_total / $available_time);
$rows[] = array(
phutil_tag('em', array(), pht('Queue Utilization (Approximate)')),
sprintf('%.1f%%', 100 * $used_time),
null,
+ null,
);
}
$completed_table = new AphrontTableView($rows);
$completed_table->setNoDataString(
pht('No tasks have completed in the last 15 minutes.'));
$completed_table->setHeaders(
array(
pht('Class'),
pht('Count'),
- pht('Avg'),
+ pht('Average Duration'),
+ pht('Average Queue Time'),
));
$completed_table->setColumnClasses(
array(
'wide',
'n',
'n',
+ 'n',
));
$completed_panel = id(new PHUIObjectBoxView())
->setHeaderText(pht('Recently Completed Tasks (Last 15m)'))
->setTable($completed_table);
$daemon_table = id(new PhabricatorDaemonLogListView())
->setUser($viewer)
->setDaemonLogs($logs);
$daemon_panel = id(new PHUIObjectBoxView())
->setHeaderText(pht('Active Daemons'))
->setTable($daemon_table);
$tasks = id(new PhabricatorWorkerLeaseQuery())
->setSkipLease(true)
->withLeasedTasks(true)
->setLimit(100)
->execute();
$tasks_table = id(new PhabricatorDaemonTasksTableView())
->setTasks($tasks)
->setNoDataString(pht('No tasks are leased by workers.'));
$leased_panel = id(new PHUIObjectBoxView())
->setHeaderText(pht('Leased Tasks'))
->setTable($tasks_table);
$task_table = new PhabricatorWorkerActiveTask();
$queued = queryfx_all(
$task_table->establishConnection('r'),
'SELECT taskClass, count(*) N FROM %T GROUP BY taskClass
ORDER BY N DESC',
$task_table->getTableName());
$rows = array();
foreach ($queued as $row) {
$rows[] = array(
$row['taskClass'],
number_format($row['N']),
);
}
$queued_table = new AphrontTableView($rows);
$queued_table->setHeaders(
array(
pht('Class'),
pht('Count'),
));
$queued_table->setColumnClasses(
array(
'wide',
'n',
));
$queued_table->setNoDataString(pht('Task queue is empty.'));
$queued_panel = new PHUIObjectBoxView();
$queued_panel->setHeaderText(pht('Queued Tasks'));
$queued_panel->setTable($queued_table);
$upcoming = id(new PhabricatorWorkerLeaseQuery())
->setLimit(10)
->setSkipLease(true)
->execute();
$upcoming_panel = id(new PHUIObjectBoxView())
->setHeaderText(pht('Next In Queue'))
->setTable(
id(new PhabricatorDaemonTasksTableView())
->setTasks($upcoming)
->setNoDataString(pht('Task queue is empty.')));
$triggers = id(new PhabricatorWorkerTriggerQuery())
->setViewer($viewer)
->setOrder(PhabricatorWorkerTriggerQuery::ORDER_EXECUTION)
->withNextEventBetween(0, null)
->needEvents(true)
->setLimit(10)
->execute();
$triggers_table = $this->buildTriggersTable($triggers);
$triggers_panel = id(new PHUIObjectBoxView())
->setHeaderText(pht('Upcoming Triggers'))
->setTable($triggers_table);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Console'));
$nav = $this->buildSideNavView();
$nav->selectFilter('/');
$nav->appendChild(
array(
$crumbs,
$completed_panel,
$daemon_panel,
$queued_panel,
$leased_panel,
$upcoming_panel,
$triggers_panel,
));
return $this->newPage()
->setTitle(pht('Console'))
->appendChild($nav);
}
private function buildTriggersTable(array $triggers) {
$viewer = $this->getViewer();
$rows = array();
foreach ($triggers as $trigger) {
$event = $trigger->getEvent();
if ($event) {
$last_epoch = $event->getLastEventEpoch();
$next_epoch = $event->getNextEventEpoch();
} else {
$last_epoch = null;
$next_epoch = null;
}
$rows[] = array(
$trigger->getID(),
$trigger->getClockClass(),
$trigger->getActionClass(),
$last_epoch ? phabricator_datetime($last_epoch, $viewer) : null,
$next_epoch ? phabricator_datetime($next_epoch, $viewer) : null,
);
}
return id(new AphrontTableView($rows))
->setNoDataString(pht('There are no upcoming event triggers.'))
->setHeaders(
array(
pht('ID'),
pht('Clock'),
pht('Action'),
pht('Last'),
pht('Next'),
))
->setColumnClasses(
array(
'',
'',
'wide',
'date',
'date',
));
}
}
diff --git a/src/applications/daemon/controller/PhabricatorDaemonLogEventViewController.php b/src/applications/daemon/controller/PhabricatorDaemonLogEventViewController.php
deleted file mode 100644
index 208a20b9a..000000000
--- a/src/applications/daemon/controller/PhabricatorDaemonLogEventViewController.php
+++ /dev/null
@@ -1,47 +0,0 @@
-<?php
-
-final class PhabricatorDaemonLogEventViewController
- extends PhabricatorDaemonController {
-
- public function handleRequest(AphrontRequest $request) {
- $id = $request->getURIData('id');
-
- $event = id(new PhabricatorDaemonLogEvent())->load($id);
- if (!$event) {
- return new Aphront404Response();
- }
-
- $event_view = id(new PhabricatorDaemonLogEventsView())
- ->setEvents(array($event))
- ->setUser($request->getUser())
- ->setCombinedLog(true)
- ->setShowFullMessage(true);
-
- $log_panel = id(new PHUIObjectBoxView())
- ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
- ->appendChild($event_view);
-
- $daemon_id = $event->getLogID();
-
- $crumbs = $this->buildApplicationCrumbs()
- ->addTextCrumb(
- pht('Daemon %s', $daemon_id),
- $this->getApplicationURI("log/{$daemon_id}/"))
- ->addTextCrumb(pht('Event %s', $event->getID()))
- ->setBorder(true);
-
- $header = id(new PHUIHeaderView())
- ->setHeader(pht('Combined Log'))
- ->setHeaderIcon('fa-file-text');
-
- $view = id(new PHUITwoColumnView())
- ->setHeader($header)
- ->setFooter($log_panel);
-
- return $this->newPage()
- ->setTitle(pht('Combined Daemon Log'))
- ->appendChild($view);
-
- }
-
-}
diff --git a/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php b/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php
deleted file mode 100644
index 039906e71..000000000
--- a/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php
+++ /dev/null
@@ -1,131 +0,0 @@
-<?php
-
-final class PhabricatorDaemonLogEventsView extends AphrontView {
-
- private $events;
- private $combinedLog;
- private $showFullMessage;
-
-
- public function setShowFullMessage($show_full_message) {
- $this->showFullMessage = $show_full_message;
- return $this;
- }
-
- public function setEvents(array $events) {
- assert_instances_of($events, 'PhabricatorDaemonLogEvent');
- $this->events = $events;
- return $this;
- }
-
- public function setCombinedLog($is_combined) {
- $this->combinedLog = $is_combined;
- return $this;
- }
-
- public function render() {
- $viewer = $this->getViewer();
- $rows = array();
-
- foreach ($this->events as $event) {
-
- // Limit display log size. If a daemon gets stuck in an output loop this
- // page can be like >100MB if we don't truncate stuff. Try to do cheap
- // line-based truncation first, and fall back to expensive UTF-8 character
- // truncation if that doesn't get things short enough.
-
- $message = $event->getMessage();
- $more = null;
-
- if (!$this->showFullMessage) {
- $more_lines = null;
- $more_chars = null;
- $line_limit = 12;
- if (substr_count($message, "\n") > $line_limit) {
- $message = explode("\n", $message);
- $more_lines = count($message) - $line_limit;
- $message = array_slice($message, 0, $line_limit);
- $message = implode("\n", $message);
- }
-
- $char_limit = 8192;
- if (strlen($message) > $char_limit) {
- $message = phutil_utf8v($message);
- $more_chars = count($message) - $char_limit;
- $message = array_slice($message, 0, $char_limit);
- $message = implode('', $message);
- }
-
- if ($more_chars) {
- $more = new PhutilNumber($more_chars);
- $more = pht('Show %d more character(s)...', $more);
- } else if ($more_lines) {
- $more = new PhutilNumber($more_lines);
- $more = pht('Show %d more line(s)...', $more);
- }
-
- if ($more) {
- $id = $event->getID();
- $more = array(
- "\n...\n",
- phutil_tag(
- 'a',
- array(
- 'href' => "/daemon/event/{$id}/",
- ),
- $more),
- );
- }
- }
-
- $row = array(
- $event->getLogType(),
- phabricator_date($event->getEpoch(), $viewer),
- phabricator_time($event->getEpoch(), $viewer),
- array(
- $message,
- $more,
- ),
- );
-
- if ($this->combinedLog) {
- array_unshift(
- $row,
- phutil_tag(
- 'a',
- array(
- 'href' => '/daemon/log/'.$event->getLogID().'/',
- ),
- pht('Daemon %s', $event->getLogID())));
- }
-
- $rows[] = $row;
- }
-
- $classes = array(
- '',
- '',
- 'right',
- 'wide prewrap',
- );
-
- $headers = array(
- 'Type',
- 'Date',
- 'Time',
- 'Message',
- );
-
- if ($this->combinedLog) {
- array_unshift($classes, 'pri');
- array_unshift($headers, 'Daemon');
- }
-
- $log_table = new AphrontTableView($rows);
- $log_table->setHeaders($headers);
- $log_table->setColumnClasses($classes);
-
- return $log_table->render();
- }
-
-}
diff --git a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php
index dfe328933..fc62c4d5c 100644
--- a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php
+++ b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php
@@ -1,357 +1,357 @@
<?php
final class PhabricatorDashboardPanelRenderingEngine extends Phobject {
const HEADER_MODE_NORMAL = 'normal';
const HEADER_MODE_NONE = 'none';
const HEADER_MODE_EDIT = 'edit';
private $panel;
private $panelPHID;
private $viewer;
private $enableAsyncRendering;
private $parentPanelPHIDs;
private $headerMode = self::HEADER_MODE_NORMAL;
private $dashboardID;
private $movable = true;
public function setDashboardID($id) {
$this->dashboardID = $id;
return $this;
}
public function getDashboardID() {
return $this->dashboardID;
}
public function setHeaderMode($header_mode) {
$this->headerMode = $header_mode;
return $this;
}
public function getHeaderMode() {
return $this->headerMode;
}
/**
* Allow the engine to render the panel via Ajax.
*/
public function setEnableAsyncRendering($enable) {
$this->enableAsyncRendering = $enable;
return $this;
}
public function setParentPanelPHIDs(array $parents) {
$this->parentPanelPHIDs = $parents;
return $this;
}
public function getParentPanelPHIDs() {
return $this->parentPanelPHIDs;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setPanel(PhabricatorDashboardPanel $panel) {
$this->panel = $panel;
return $this;
}
public function setMovable($movable) {
$this->movable = $movable;
return $this;
}
public function getMovable() {
return $this->movable;
}
public function getPanel() {
return $this->panel;
}
public function setPanelPHID($panel_phid) {
$this->panelPHID = $panel_phid;
return $this;
}
public function getPanelPHID() {
return $this->panelPHID;
}
public function renderPanel() {
$panel = $this->getPanel();
if (!$panel) {
return $this->renderErrorPanel(
pht('Missing or Restricted Panel'),
pht(
'This panel does not exist, or you do not have permission '.
'to see it.'));
}
$panel_type = $panel->getImplementation();
if (!$panel_type) {
return $this->renderErrorPanel(
$panel->getName(),
pht(
'This panel has type "%s", but that panel type is not known to '.
'Phabricator.',
$panel->getPanelType()));
}
try {
$this->detectRenderingCycle($panel);
if ($this->enableAsyncRendering) {
if ($panel_type->shouldRenderAsync()) {
return $this->renderAsyncPanel();
}
}
return $this->renderNormalPanel();
} catch (Exception $ex) {
return $this->renderErrorPanel(
$panel->getName(),
pht(
'%s: %s',
phutil_tag('strong', array(), get_class($ex)),
$ex->getMessage()));
}
}
private function renderNormalPanel() {
$panel = $this->getPanel();
$panel_type = $panel->getImplementation();
$content = $panel_type->renderPanelContent(
$this->getViewer(),
$panel,
$this);
$header = $this->renderPanelHeader();
return $this->renderPanelDiv(
$content,
$header);
}
private function renderAsyncPanel() {
$panel = $this->getPanel();
$panel_id = celerity_generate_unique_node_id();
$dashboard_id = $this->getDashboardID();
Javelin::initBehavior(
'dashboard-async-panel',
array(
'panelID' => $panel_id,
'parentPanelPHIDs' => $this->getParentPanelPHIDs(),
'headerMode' => $this->getHeaderMode(),
'dashboardID' => $dashboard_id,
'uri' => '/dashboard/panel/render/'.$panel->getID().'/',
));
$header = $this->renderPanelHeader();
$content = id(new PHUIPropertyListView())
->addTextContent(pht('Loading...'));
return $this->renderPanelDiv(
$content,
$header,
$panel_id);
}
private function renderErrorPanel($title, $body) {
switch ($this->getHeaderMode()) {
case self::HEADER_MODE_NONE:
$header = null;
break;
case self::HEADER_MODE_EDIT:
$header = id(new PHUIHeaderView())
->setHeader($title);
$header = $this->addPanelHeaderActions($header);
break;
case self::HEADER_MODE_NORMAL:
default:
$header = id(new PHUIHeaderView())
->setHeader($title);
break;
}
$icon = id(new PHUIIconView())
->setIcon('fa-warning red msr');
$content = id(new PHUIBoxView())
->addClass('dashboard-box')
->addMargin(PHUI::MARGIN_MEDIUM)
->appendChild($icon)
->appendChild($body);
return $this->renderPanelDiv(
$content,
$header);
}
private function renderPanelDiv(
$content,
$header = null,
$id = null) {
require_celerity_resource('phabricator-dashboard-css');
$panel = $this->getPanel();
if (!$id) {
$id = celerity_generate_unique_node_id();
}
$box = new PHUIObjectBoxView();
$interface = 'PhabricatorApplicationSearchResultView';
if ($content instanceof $interface) {
if ($content->getObjectList()) {
$box->setObjectList($content->getObjectList());
}
if ($content->getTable()) {
$box->setTable($content->getTable());
}
if ($content->getContent()) {
$box->appendChild($content->getContent());
}
} else {
$box->appendChild($content);
}
$box
->setHeader($header)
->setID($id)
->addClass('dashboard-box')
->addSigil('dashboard-panel');
if ($this->getMovable()) {
$box->addSigil('panel-movable');
}
if ($panel) {
$box->setMetadata(
array(
'objectPHID' => $panel->getPHID(),
));
}
return phutil_tag_div('dashboard-pane', $box);
}
private function renderPanelHeader() {
$panel = $this->getPanel();
switch ($this->getHeaderMode()) {
case self::HEADER_MODE_NONE:
$header = null;
break;
case self::HEADER_MODE_EDIT:
$header = id(new PHUIHeaderView())
->setHeader($panel->getName());
$header = $this->addPanelHeaderActions($header);
break;
case self::HEADER_MODE_NORMAL:
default:
$header = id(new PHUIHeaderView())
->setHeader($panel->getName());
$panel_type = $panel->getImplementation();
$header = $panel_type->adjustPanelHeader(
$this->getViewer(),
$panel,
$this,
$header);
break;
}
return $header;
}
private function addPanelHeaderActions(
PHUIHeaderView $header) {
$panel = $this->getPanel();
$dashboard_id = $this->getDashboardID();
if ($panel) {
$panel_id = $panel->getID();
$edit_uri = "/dashboard/panel/edit/{$panel_id}/";
$edit_uri = new PhutilURI($edit_uri);
if ($dashboard_id) {
- $edit_uri->setQueryParam('dashboardID', $dashboard_id);
+ $edit_uri->replaceQueryParam('dashboardID', $dashboard_id);
}
$action_edit = id(new PHUIIconView())
->setIcon('fa-pencil')
->setWorkflow(true)
->setHref((string)$edit_uri);
$header->addActionItem($action_edit);
}
if ($dashboard_id) {
$panel_phid = $this->getPanelPHID();
$remove_uri = "/dashboard/removepanel/{$dashboard_id}/";
$remove_uri = id(new PhutilURI($remove_uri))
- ->setQueryParam('panelPHID', $panel_phid);
+ ->replaceQueryParam('panelPHID', $panel_phid);
$action_remove = id(new PHUIIconView())
->setIcon('fa-trash-o')
->setHref((string)$remove_uri)
->setWorkflow(true);
$header->addActionItem($action_remove);
}
return $header;
}
/**
* Detect graph cycles in panels, and deeply nested panels.
*
* This method throws if the current rendering stack is too deep or contains
* a cycle. This can happen if you embed layout panels inside each other,
* build a big stack of panels, or embed a panel in remarkup inside another
* panel. Generally, all of this stuff is ridiculous and we just want to
* shut it down.
*
* @param PhabricatorDashboardPanel Panel being rendered.
* @return void
*/
private function detectRenderingCycle(PhabricatorDashboardPanel $panel) {
if ($this->parentPanelPHIDs === null) {
throw new PhutilInvalidStateException('setParentPanelPHIDs');
}
$max_depth = 4;
if (count($this->parentPanelPHIDs) >= $max_depth) {
throw new Exception(
pht(
'To render more than %s levels of panels nested inside other '.
'panels, purchase a subscription to Phabricator Gold.',
new PhutilNumber($max_depth)));
}
if (in_array($panel->getPHID(), $this->parentPanelPHIDs)) {
throw new Exception(
pht(
'You awake in a twisting maze of mirrors, all alike. '.
'You are likely to be eaten by a graph cycle. '.
'Should you escape alive, you resolve to be more careful about '.
'putting dashboard panels inside themselves.'));
}
}
}
diff --git a/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php
index ba6aace97..9f6481c05 100644
--- a/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php
+++ b/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php
@@ -1,147 +1,147 @@
<?php
final class PhabricatorDashboardRenderingEngine extends Phobject {
private $dashboard;
private $viewer;
private $arrangeMode;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function setDashboard(PhabricatorDashboard $dashboard) {
$this->dashboard = $dashboard;
return $this;
}
public function setArrangeMode($mode) {
$this->arrangeMode = $mode;
return $this;
}
public function renderDashboard() {
require_celerity_resource('phabricator-dashboard-css');
$dashboard = $this->dashboard;
$viewer = $this->viewer;
$layout_config = $dashboard->getLayoutConfigObject();
$panel_grid_locations = $layout_config->getPanelLocations();
$panels = mpull($dashboard->getPanels(), null, 'getPHID');
$dashboard_id = celerity_generate_unique_node_id();
$result = id(new AphrontMultiColumnView())
->setID($dashboard_id)
->setFluidLayout(true)
->setGutter(AphrontMultiColumnView::GUTTER_LARGE);
if ($this->arrangeMode) {
$h_mode = PhabricatorDashboardPanelRenderingEngine::HEADER_MODE_EDIT;
} else {
$h_mode = PhabricatorDashboardPanelRenderingEngine::HEADER_MODE_NORMAL;
}
foreach ($panel_grid_locations as $column => $panel_column_locations) {
$panel_phids = $panel_column_locations;
// TODO: This list may contain duplicates when the dashboard itself
// does not? Perhaps this is related to T10612. For now, just unique
// the list before moving on.
$panel_phids = array_unique($panel_phids);
$column_result = array();
foreach ($panel_phids as $panel_phid) {
$panel_engine = id(new PhabricatorDashboardPanelRenderingEngine())
->setViewer($viewer)
->setDashboardID($dashboard->getID())
->setEnableAsyncRendering(true)
->setPanelPHID($panel_phid)
->setParentPanelPHIDs(array())
->setHeaderMode($h_mode);
$panel = idx($panels, $panel_phid);
if ($panel) {
$panel_engine->setPanel($panel);
}
$column_result[] = $panel_engine->renderPanel();
}
$column_class = $layout_config->getColumnClass(
$column,
$this->arrangeMode);
if ($this->arrangeMode) {
$column_result[] = $this->renderAddPanelPlaceHolder($column);
$column_result[] = $this->renderAddPanelUI($column);
}
$result->addColumn(
$column_result,
$column_class,
$sigil = 'dashboard-column',
$metadata = array('columnID' => $column));
}
if ($this->arrangeMode) {
Javelin::initBehavior(
'dashboard-move-panels',
array(
'dashboardID' => $dashboard_id,
'moveURI' => '/dashboard/movepanel/'.$dashboard->getID().'/',
));
}
$view = id(new PHUIBoxView())
->addClass('dashboard-view')
->appendChild($result);
return $view;
}
private function renderAddPanelPlaceHolder($column) {
$dashboard = $this->dashboard;
$panels = $dashboard->getPanels();
return javelin_tag(
'span',
array(
'sigil' => 'workflow',
'class' => 'drag-ghost dashboard-panel-placeholder',
),
pht('This column does not have any panels yet.'));
}
private function renderAddPanelUI($column) {
$dashboard_id = $this->dashboard->getID();
$create_uri = id(new PhutilURI('/dashboard/panel/create/'))
- ->setQueryParam('dashboardID', $dashboard_id)
- ->setQueryParam('column', $column);
+ ->replaceQueryParam('dashboardID', $dashboard_id)
+ ->replaceQueryParam('column', $column);
$add_uri = id(new PhutilURI('/dashboard/addpanel/'.$dashboard_id.'/'))
- ->setQueryParam('column', $column);
+ ->replaceQueryParam('column', $column);
$create_button = id(new PHUIButtonView())
->setTag('a')
->setHref($create_uri)
->setWorkflow(true)
->setText(pht('Create Panel'))
->addClass(PHUI::MARGIN_MEDIUM);
$add_button = id(new PHUIButtonView())
->setTag('a')
->setHref($add_uri)
->setWorkflow(true)
->setText(pht('Add Existing Panel'))
->addClass(PHUI::MARGIN_MEDIUM);
return phutil_tag(
'div',
array(
'style' => 'text-align: center;',
),
array(
$create_button,
$add_button,
));
}
}
diff --git a/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php b/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php
index 0781d71b1..90fe93e2b 100644
--- a/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php
+++ b/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php
@@ -1,152 +1,224 @@
<?php
final class PhabricatorDashboardQueryPanelType
extends PhabricatorDashboardPanelType {
public function getPanelTypeKey() {
return 'query';
}
public function getPanelTypeName() {
return pht('Query Panel');
}
public function getIcon() {
return 'fa-search';
}
public function getPanelTypeDescription() {
return pht(
'Show results of a search query, like the most recently filed tasks or '.
'revisions you need to review.');
}
public function getFieldSpecifications() {
return array(
'class' => array(
'name' => pht('Search For'),
'type' => 'search.application',
),
'key' => array(
'name' => pht('Query'),
'type' => 'search.query',
'control.application' => 'class',
),
'limit' => array(
'name' => pht('Limit'),
'caption' => pht('Leave this blank for the default number of items.'),
'type' => 'text',
),
);
}
public function initializeFieldsFromRequest(
PhabricatorDashboardPanel $panel,
PhabricatorCustomFieldList $field_list,
AphrontRequest $request) {
$map = array();
if (strlen($request->getStr('engine'))) {
$map['class'] = $request->getStr('engine');
}
if (strlen($request->getStr('query'))) {
$map['key'] = $request->getStr('query');
}
$full_map = array();
foreach ($map as $key => $value) {
$full_map["std:dashboard:core:{$key}"] = $value;
}
foreach ($field_list->getFields() as $field) {
$field_key = $field->getFieldKey();
if (isset($full_map[$field_key])) {
$field->setValueFromStorage($full_map[$field_key]);
}
}
}
public function renderPanelContent(
PhabricatorUser $viewer,
PhabricatorDashboardPanel $panel,
PhabricatorDashboardPanelRenderingEngine $engine) {
$engine = $this->getSearchEngine($panel);
$engine->setViewer($viewer);
$engine->setContext(PhabricatorApplicationSearchEngine::CONTEXT_PANEL);
$key = $panel->getProperty('key');
if ($engine->isBuiltinQuery($key)) {
$saved = $engine->buildSavedQueryFromBuiltin($key);
} else {
$saved = id(new PhabricatorSavedQueryQuery())
->setViewer($viewer)
->withEngineClassNames(array(get_class($engine)))
->withQueryKeys(array($key))
->executeOne();
}
if (!$saved) {
throw new Exception(
pht(
'Query "%s" is unknown to application search engine "%s"!',
$key,
get_class($engine)));
}
$query = $engine->buildQueryFromSavedQuery($saved);
$pager = $engine->newPagerForSavedQuery($saved);
if ($panel->getProperty('limit')) {
$limit = (int)$panel->getProperty('limit');
if ($pager->getPageSize() !== 0xFFFF) {
$pager->setPageSize($limit);
}
}
+ $query->setReturnPartialResultsOnOverheat(true);
+
$results = $engine->executeQuery($query, $pager);
+ $results_view = $engine->renderResults($results, $saved);
+
+ $is_overheated = $query->getIsOverheated();
+ $overheated_view = null;
+ if ($is_overheated) {
+ $content = $results_view->getContent();
+
+ $overheated_message =
+ PhabricatorApplicationSearchController::newOverheatedError(
+ (bool)$results);
+
+ $overheated_warning = id(new PHUIInfoView())
+ ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
+ ->setTitle(pht('Query Overheated'))
+ ->setErrors(
+ array(
+ $overheated_message,
+ ));
+
+ $overheated_box = id(new PHUIBoxView())
+ ->addClass('mmt mmb')
+ ->appendChild($overheated_warning);
+
+ $content = array($content, $overheated_box);
+ $results_view->setContent($content);
+ }
+
+ // TODO: A small number of queries, including "Notifications" and "Search",
+ // use an offset pager which has a slightly different API. Some day, we
+ // should unify these.
+ if ($pager instanceof PHUIPagerView) {
+ $has_more = $pager->getHasMorePages();
+ } else {
+ $has_more = $pager->getHasMoreResults();
+ }
+
+ if ($has_more) {
+ $item_list = $results_view->getObjectList();
+
+ $more_href = $engine->getQueryResultsPageURI($key);
+ if ($item_list) {
+ $item_list->newTailButton()
+ ->setHref($more_href);
+ } else {
+ // For search engines that do not return an object list, add a fake
+ // one to the end so we can render a "View All Results" button that
+ // looks like it does in normal applications. At time of writing,
+ // several major applications like Maniphest (which has group headers)
+ // and Feed (which uses custom rendering) don't return simple lists.
+
+ $content = $results_view->getContent();
- return $engine->renderResults($results, $saved);
+ $more_list = id(new PHUIObjectItemListView())
+ ->setAllowEmptyList(true);
+
+ $more_list->newTailButton()
+ ->setHref($more_href);
+
+ $content = array($content, $more_list);
+ $results_view->setContent($content);
+ }
+ }
+
+ return $results_view;
}
public function adjustPanelHeader(
PhabricatorUser $viewer,
PhabricatorDashboardPanel $panel,
PhabricatorDashboardPanelRenderingEngine $engine,
PHUIHeaderView $header) {
$search_engine = $this->getSearchEngine($panel);
$key = $panel->getProperty('key');
$href = $search_engine->getQueryResultsPageURI($key);
+
$icon = id(new PHUIIconView())
- ->setIcon('fa-search')
- ->setHref($href);
- $header->addActionItem($icon);
+ ->setIcon('fa-search');
+
+ $button = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setText(pht('View All'))
+ ->setIcon($icon)
+ ->setHref($href)
+ ->setColor(PHUIButtonView::GREY);
+
+ $header->addActionLink($button);
return $header;
}
private function getSearchEngine(PhabricatorDashboardPanel $panel) {
$class = $panel->getProperty('class');
$engine = PhabricatorApplicationSearchEngine::getEngineByClassName($class);
if (!$engine) {
throw new Exception(
pht(
'The application search engine "%s" is not known to Phabricator!',
$class));
}
if (!$engine->canUseInPanelContext()) {
throw new Exception(
pht(
'Application search engines of class "%s" can not be used to build '.
'dashboard panels.',
$class));
}
return $engine;
}
}
diff --git a/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php b/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php
index c278ff0c6..ce93bbe65 100644
--- a/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php
+++ b/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php
@@ -1,121 +1,114 @@
<?php
final class DifferentialParseRenderTestCase extends PhabricatorTestCase {
private function getTestDataDirectory() {
return dirname(__FILE__).'/data/';
}
public function testParseRender() {
$dir = $this->getTestDataDirectory();
foreach (Filesystem::listDirectory($dir, $show_hidden = false) as $file) {
if (!preg_match('/\.diff$/', $file)) {
continue;
}
$data = Filesystem::readFile($dir.$file);
+ // Strip trailing "~" characters from inputs so they may contain
+ // trailing whitespace.
+ $data = preg_replace('/~$/m', '', $data);
+
$opt_file = $dir.$file.'.options';
if (Filesystem::pathExists($opt_file)) {
$options = Filesystem::readFile($opt_file);
try {
$options = phutil_json_decode($options);
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht('Invalid options file: %s.', $opt_file),
$ex);
}
} else {
$options = array();
}
foreach (array('one', 'two') as $type) {
$this->runParser($type, $data, $file, 'expect');
$this->runParser($type, $data, $file, 'unshielded');
- $this->runParser($type, $data, $file, 'whitespace');
}
}
}
private function runParser($type, $data, $file, $extension) {
$dir = $this->getTestDataDirectory();
$test_file = $dir.$file.'.'.$type.'.'.$extension;
if (!Filesystem::pathExists($test_file)) {
return;
}
$unshielded = false;
- $whitespace = false;
switch ($extension) {
case 'unshielded':
$unshielded = true;
break;
- case 'whitespace';
- $unshielded = true;
- $whitespace = true;
- break;
}
$parsers = $this->buildChangesetParsers($type, $data, $file);
- $actual = $this->renderParsers($parsers, $unshielded, $whitespace);
+ $actual = $this->renderParsers($parsers, $unshielded);
$expect = Filesystem::readFile($test_file);
$this->assertEqual($expect, $actual, basename($test_file));
}
- private function renderParsers(array $parsers, $unshield, $whitespace) {
+ private function renderParsers(array $parsers, $unshield) {
$result = array();
foreach ($parsers as $parser) {
if ($unshield) {
$s_range = 0;
$e_range = 0xFFFF;
} else {
$s_range = null;
$e_range = null;
}
- if ($whitespace) {
- $parser->setWhitespaceMode(
- DifferentialChangesetParser::WHITESPACE_SHOW_ALL);
- }
-
$result[] = $parser->render($s_range, $e_range, array());
}
return implode(str_repeat('~', 80)."\n", $result);
}
private function buildChangesetParsers($type, $data, $file) {
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($data);
$diff = DifferentialDiff::newFromRawChanges(
PhabricatorUser::getOmnipotentUser(),
$changes);
$changesets = $diff->getChangesets();
$engine = new PhabricatorMarkupEngine();
$engine->setViewer(new PhabricatorUser());
$parsers = array();
foreach ($changesets as $changeset) {
$cparser = new DifferentialChangesetParser();
$cparser->setUser(new PhabricatorUser());
$cparser->setDisableCache(true);
$cparser->setChangeset($changeset);
$cparser->setMarkupEngine($engine);
if ($type == 'one') {
$cparser->setRenderer(new DifferentialChangesetOneUpTestRenderer());
} else if ($type == 'two') {
$cparser->setRenderer(new DifferentialChangesetTwoUpTestRenderer());
} else {
throw new Exception(pht('Unknown renderer type "%s"!', $type));
}
$parsers[] = $cparser;
}
return $parsers;
}
}
diff --git a/src/applications/differential/__tests__/data/generated.diff b/src/applications/differential/__tests__/data/generated.diff
index 7846c9a49..c130993cf 100644
--- a/src/applications/differential/__tests__/data/generated.diff
+++ b/src/applications/differential/__tests__/data/generated.diff
@@ -1,10 +1,10 @@
diff --git a/GENERATED b/GENERATED
index 5dcff7f..eff82ef 100644
--- a/GENERATED
+++ b/GENERATED
@@ -1,4 +1,4 @@
@generated
-
+ ~
-This is a generated file.
+This is a generated file, full of generated code.
diff --git a/src/applications/differential/__tests__/data/generated.diff.one.unshielded b/src/applications/differential/__tests__/data/generated.diff.one.unshielded
index ca4b1b167..acfa701c8 100644
--- a/src/applications/differential/__tests__/data/generated.diff.one.unshielded
+++ b/src/applications/differential/__tests__/data/generated.diff.one.unshielded
@@ -1,6 +1,5 @@
N 1 . @generated\n~
-O 2 - \n~
+N 2 . \n~
O 3 - This is a generated file.\n~
-N 2 + \n~
N 3 + This is a generated file{(, full of generated code)}.\n~
N 4 . \n~
diff --git a/src/applications/differential/__tests__/data/generated.diff.two.unshielded b/src/applications/differential/__tests__/data/generated.diff.two.unshielded
index 967f6220d..183a0b6ed 100644
--- a/src/applications/differential/__tests__/data/generated.diff.two.unshielded
+++ b/src/applications/differential/__tests__/data/generated.diff.two.unshielded
@@ -1,8 +1,8 @@
O 1 . @generated\n~
N 1 . @generated\n~
-O 2 - \n~
-N 2 + \n~
+O 2 . \n~
+N 2 . \n~
O 3 - This is a generated file.\n~
N 3 + This is a generated file{(, full of generated code)}.\n~
O 4 . \n~
N 4 . \n~
diff --git a/src/applications/differential/__tests__/data/whitespace.diff.one.expect b/src/applications/differential/__tests__/data/whitespace.diff.one.expect
index 5b229959d..87ad1dcdd 100644
--- a/src/applications/differential/__tests__/data/whitespace.diff.one.expect
+++ b/src/applications/differential/__tests__/data/whitespace.diff.one.expect
@@ -1,5 +1,6 @@
CTYPE 2 1 (unforced)
WHITESPACE
WHITESPACE
-
-SHIELD (whitespace) This file was changed only by adding or removing whitespace.
+O 1 - -=[-Rocket-Ship>\n~
+N 1 + {> )}-=[-Rocket-Ship>\n~
diff --git a/src/applications/differential/__tests__/data/whitespace.diff.one.whitespace b/src/applications/differential/__tests__/data/whitespace.diff.one.whitespace
index f4a5af6f3..db43e0215 100644
--- a/src/applications/differential/__tests__/data/whitespace.diff.one.whitespace
+++ b/src/applications/differential/__tests__/data/whitespace.diff.one.whitespace
@@ -1,2 +1,2 @@
O 1 - -=[-Rocket-Ship>\n~
-N 1 + {( )}-=[-Rocket-Ship>\n~
+N 1 + {> )}-=[-Rocket-Ship>\n~
diff --git a/src/applications/differential/__tests__/data/whitespace.diff.two.expect b/src/applications/differential/__tests__/data/whitespace.diff.two.expect
index 5b229959d..87ad1dcdd 100644
--- a/src/applications/differential/__tests__/data/whitespace.diff.two.expect
+++ b/src/applications/differential/__tests__/data/whitespace.diff.two.expect
@@ -1,5 +1,6 @@
CTYPE 2 1 (unforced)
WHITESPACE
WHITESPACE
-
-SHIELD (whitespace) This file was changed only by adding or removing whitespace.
+O 1 - -=[-Rocket-Ship>\n~
+N 1 + {> )}-=[-Rocket-Ship>\n~
diff --git a/src/applications/differential/__tests__/data/whitespace.diff.two.whitespace b/src/applications/differential/__tests__/data/whitespace.diff.two.whitespace
index f4a5af6f3..db43e0215 100644
--- a/src/applications/differential/__tests__/data/whitespace.diff.two.whitespace
+++ b/src/applications/differential/__tests__/data/whitespace.diff.two.whitespace
@@ -1,2 +1,2 @@
O 1 - -=[-Rocket-Ship>\n~
-N 1 + {( )}-=[-Rocket-Ship>\n~
+N 1 + {> )}-=[-Rocket-Ship>\n~
diff --git a/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php b/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php
index ec2099f8d..9634756da 100644
--- a/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php
+++ b/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php
@@ -1,266 +1,254 @@
<?php
final class PhabricatorDifferentialConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Differential');
}
public function getDescription() {
return pht('Configure Differential code review.');
}
public function getIcon() {
return 'fa-cog';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
$caches_href = PhabricatorEnv::getDoclink('Managing Caches');
$custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType';
$fields = array(
new DifferentialSummaryField(),
new DifferentialTestPlanField(),
new DifferentialReviewersField(),
new DifferentialProjectReviewersField(),
new DifferentialRepositoryField(),
new DifferentialManiphestTasksField(),
new DifferentialCommitsField(),
new DifferentialJIRAIssuesField(),
new DifferentialAsanaRepresentationField(),
new DifferentialChangesSinceLastUpdateField(),
new DifferentialBranchField(),
new DifferentialBlameRevisionField(),
new DifferentialPathField(),
new DifferentialHostField(),
new DifferentialLintField(),
new DifferentialUnitField(),
new DifferentialRevertPlanField(),
);
$default_fields = array();
foreach ($fields as $field) {
$default_fields[$field->getFieldKey()] = array(
'disabled' => $field->shouldDisableByDefault(),
);
}
$inline_description = $this->deformat(
pht(<<<EOHELP
To include patches inline in email bodies, set this option to a positive
integer. Patches will be inlined if they are at most that many lines and at
most 256 times that many bytes.
For example, a value of 100 means "inline patches if they are at not more than
100 lines long and not more than 25,600 bytes large".
By default, patches are not inlined.
EOHELP
));
return array(
$this->newOption(
'differential.fields',
$custom_field_type,
$default_fields)
->setCustomData(
id(new DifferentialRevision())->getCustomFieldBaseClass())
->setDescription(
pht(
"Select and reorder revision fields.\n\n".
"NOTE: This feature is under active development and subject ".
"to change.")),
- $this->newOption(
- 'differential.whitespace-matters',
- 'list<regex>',
- array(
- '/\.py$/',
- '/\.l?hs$/',
- '/\.ya?ml$/',
- ))
- ->setDescription(
- pht(
- "List of file regexps where whitespace is meaningful and should ".
- "not use 'ignore-all' by default")),
$this->newOption('differential.require-test-plan-field', 'bool', true)
->setBoolOptions(
array(
pht("Require 'Test Plan' field"),
pht("Make 'Test Plan' field optional"),
))
->setSummary(pht('Require "Test Plan" field?'))
->setDescription(
pht(
"Differential has a required 'Test Plan' field by default. You ".
"can make it optional by setting this to false. You can also ".
"completely remove it above, if you prefer.")),
$this->newOption('differential.enable-email-accept', 'bool', false)
->setBoolOptions(
array(
pht('Enable Email "!accept" Action'),
pht('Disable Email "!accept" Action'),
))
->setSummary(pht('Enable or disable "!accept" action via email.'))
->setDescription(
pht(
'If inbound email is configured, users can interact with '.
'revisions by using "!actions" in email replies (for example, '.
'"!resign" or "!rethink"). However, by default, users may not '.
'"!accept" revisions via email: email authentication can be '.
'configured to be very weak, and email "!accept" is kind of '.
'sketchy and implies the revision may not actually be receiving '.
'thorough review. You can enable "!accept" by setting this '.
'option to true.')),
$this->newOption('differential.generated-paths', 'list<regex>', array())
->setSummary(pht('File regexps to treat as automatically generated.'))
->setDescription(
pht(
'List of file regexps that should be treated as if they are '.
'generated by an automatic process, and thus be hidden by '.
'default in Differential.'.
"\n\n".
'NOTE: This property is cached, so you will need to purge the '.
'cache after making changes if you want the new configuration '.
'to affect existing revisions. For instructions, see '.
'**[[ %s | Managing Caches ]]** in the documentation.',
$caches_href))
->addExample("/config\.h$/\n#(^|/)autobuilt/#", pht('Valid Setting')),
$this->newOption('differential.sticky-accept', 'bool', true)
->setBoolOptions(
array(
pht('Accepts persist across updates'),
pht('Accepts are reset by updates'),
))
->setSummary(
pht('Should "Accepted" revisions remain "Accepted" after updates?'))
->setDescription(
pht(
'Normally, when revisions that have been "Accepted" are updated, '.
'they remain "Accepted". This allows reviewers to suggest minor '.
'alterations when accepting, and encourages authors to update '.
'if they make minor changes in response to this feedback.'.
"\n\n".
'If you want updates to always require re-review, you can disable '.
'the "stickiness" of the "Accepted" status with this option. '.
'This may make the process for minor changes much more burdensome '.
'to both authors and reviewers.')),
$this->newOption('differential.allow-self-accept', 'bool', false)
->setBoolOptions(
array(
pht('Allow self-accept'),
pht('Disallow self-accept'),
))
->setSummary(pht('Allows users to accept their own revisions.'))
->setDescription(
pht(
"If you set this to true, users can accept their own revisions. ".
"This action is disabled by default because it's most likely not ".
"a behavior you want, but it proves useful if you are working ".
"alone on a project and want to make use of all of ".
"differential's features.")),
$this->newOption('differential.always-allow-close', 'bool', false)
->setBoolOptions(
array(
pht('Allow any user'),
pht('Restrict to submitter'),
))
->setSummary(pht('Allows any user to close accepted revisions.'))
->setDescription(
pht(
'If you set this to true, any user can close any revision so '.
'long as it has been accepted. This can be useful depending on '.
'your development model. For example, github-style pull requests '.
'where the reviewer is often the actual committer can benefit '.
'from turning this option to true. If false, only the submitter '.
'can close a revision.')),
$this->newOption('differential.always-allow-abandon', 'bool', false)
->setBoolOptions(
array(
pht('Allow any user'),
pht('Restrict to submitter'),
))
->setSummary(pht('Allows any user to abandon revisions.'))
->setDescription(
pht(
'If you set this to true, any user can abandon any revision. If '.
'false, only the submitter can abandon a revision.')),
$this->newOption('differential.allow-reopen', 'bool', false)
->setBoolOptions(
array(
pht('Enable reopen'),
pht('Disable reopen'),
))
->setSummary(pht('Allows any user to reopen a closed revision.'))
->setDescription(
pht(
'If you set this to true, any user can reopen a revision so '.
'long as it has been closed. This can be useful if a revision '.
'is accidentally closed or if a developer changes his or her '.
'mind after closing a revision. If it is false, reopening '.
'is not allowed.')),
$this->newOption('differential.close-on-accept', 'bool', false)
->setBoolOptions(
array(
pht('Treat Accepted Revisions as "Closed"'),
pht('Treat Accepted Revisions as "Open"'),
))
->setSummary(pht('Allows "Accepted" to act as a closed status.'))
->setDescription(
pht(
'Normally, Differential revisions remain on the dashboard when '.
'they are "Accepted", and the author then commits the changes '.
'to "Close" the revision and move it off the dashboard.'.
"\n\n".
'If you have an unusual workflow where Differential is used for '.
'post-commit review (normally called "Audit", elsewhere in '.
'Phabricator), you can set this flag to treat the "Accepted" '.
'state as a "Closed" state and end the review workflow early.'.
"\n\n".
'This sort of workflow is very unusual. Very few installs should '.
'need to change this option.')),
$this->newOption(
'metamta.differential.attach-patches',
'bool',
false)
->setBoolOptions(
array(
pht('Attach Patches'),
pht('Do Not Attach Patches'),
))
->setSummary(pht('Attach patches to email, as text attachments.'))
->setDescription(
pht(
'If you set this to true, Phabricator will attach patches to '.
'Differential mail (as text attachments). This will not work if '.
'you are using SendGrid as your mail adapter.')),
$this->newOption(
'metamta.differential.inline-patches',
'int',
0)
->setSummary(pht('Inline patches in email, as body text.'))
->setDescription($inline_description),
$this->newOption(
'metamta.differential.patch-format',
'enum',
'unified')
->setDescription(
pht('Format for inlined or attached patches.'))
->setEnumOptions(
array(
'unified' => pht('Unified'),
'git' => pht('Git'),
)),
);
}
}
diff --git a/src/applications/differential/controller/DifferentialChangesetViewController.php b/src/applications/differential/controller/DifferentialChangesetViewController.php
index c41f95139..35793a210 100644
--- a/src/applications/differential/controller/DifferentialChangesetViewController.php
+++ b/src/applications/differential/controller/DifferentialChangesetViewController.php
@@ -1,456 +1,458 @@
<?php
final class DifferentialChangesetViewController extends DifferentialController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$rendering_reference = $request->getStr('ref');
$parts = explode('/', $rendering_reference);
if (count($parts) == 2) {
list($id, $vs) = $parts;
} else {
$id = $parts[0];
$vs = 0;
}
$id = (int)$id;
$vs = (int)$vs;
$load_ids = array($id);
if ($vs && ($vs != -1)) {
$load_ids[] = $vs;
}
$changesets = id(new DifferentialChangesetQuery())
->setViewer($viewer)
->withIDs($load_ids)
->needHunks(true)
->execute();
$changesets = mpull($changesets, null, 'getID');
$changeset = idx($changesets, $id);
if (!$changeset) {
return new Aphront404Response();
}
$vs_changeset = null;
if ($vs && ($vs != -1)) {
$vs_changeset = idx($changesets, $vs);
if (!$vs_changeset) {
return new Aphront404Response();
}
}
$view = $request->getStr('view');
if ($view) {
$phid = idx($changeset->getMetadata(), "$view:binary-phid");
if ($phid) {
return id(new AphrontRedirectResponse())->setURI("/file/info/$phid/");
}
switch ($view) {
case 'new':
return $this->buildRawFileResponse($changeset, $is_new = true);
case 'old':
if ($vs_changeset) {
return $this->buildRawFileResponse($vs_changeset, $is_new = true);
}
return $this->buildRawFileResponse($changeset, $is_new = false);
default:
return new Aphront400Response();
}
}
$old = array();
$new = array();
if (!$vs) {
$right = $changeset;
$left = null;
$right_source = $right->getID();
$right_new = true;
$left_source = $right->getID();
$left_new = false;
$render_cache_key = $right->getID();
$old[] = $changeset;
$new[] = $changeset;
} else if ($vs == -1) {
$right = null;
$left = $changeset;
$right_source = $left->getID();
$right_new = false;
$left_source = $left->getID();
$left_new = true;
$render_cache_key = null;
$old[] = $changeset;
$new[] = $changeset;
} else {
$right = $changeset;
$left = $vs_changeset;
$right_source = $right->getID();
$right_new = true;
$left_source = $left->getID();
$left_new = true;
$render_cache_key = null;
$new[] = $left;
$new[] = $right;
}
if ($left) {
$left_data = $left->makeNewFile();
if ($right) {
$right_data = $right->makeNewFile();
} else {
$right_data = $left->makeOldFile();
}
$engine = new PhabricatorDifferenceEngine();
$synthetic = $engine->generateChangesetFromFileContent(
$left_data,
$right_data);
$choice = clone nonempty($left, $right);
$choice->attachHunks($synthetic->getHunks());
$changeset = $choice;
}
if ($left_new || $right_new) {
$diff_map = array();
if ($left) {
$diff_map[] = $left->getDiff();
}
if ($right) {
$diff_map[] = $right->getDiff();
}
$diff_map = mpull($diff_map, null, 'getPHID');
$buildables = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withBuildablePHIDs(array_keys($diff_map))
->withManualBuildables(false)
->needBuilds(true)
->needTargets(true)
->execute();
$buildables = mpull($buildables, null, 'getBuildablePHID');
foreach ($diff_map as $diff_phid => $changeset_diff) {
$changeset_diff->attachBuildable(idx($buildables, $diff_phid));
}
}
$coverage = null;
if ($right_new) {
$coverage = $this->loadCoverage($right);
}
$spec = $request->getStr('range');
list($range_s, $range_e, $mask) =
DifferentialChangesetParser::parseRangeSpecification($spec);
$parser = id(new DifferentialChangesetParser())
->setCoverage($coverage)
->setChangeset($changeset)
->setRenderingReference($rendering_reference)
->setRenderCacheKey($render_cache_key)
->setRightSideCommentMapping($right_source, $right_new)
->setLeftSideCommentMapping($left_source, $left_new);
$parser->readParametersFromRequest($request);
if ($left && $right) {
$parser->setOriginals($left, $right);
}
$diff = $changeset->getDiff();
$revision_id = $diff->getRevisionID();
$can_mark = false;
$object_owner_phid = null;
$revision = null;
if ($revision_id) {
$revision = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($revision_id))
->executeOne();
if ($revision) {
$can_mark = ($revision->getAuthorPHID() == $viewer->getPHID());
$object_owner_phid = $revision->getAuthorPHID();
}
}
// Load both left-side and right-side inline comments.
if ($revision) {
$query = id(new DifferentialInlineCommentQuery())
->setViewer($viewer)
->needHidden(true)
->withRevisionPHIDs(array($revision->getPHID()));
$inlines = $query->execute();
$inlines = $query->adjustInlinesForChangesets(
$inlines,
$old,
$new,
$revision);
} else {
$inlines = array();
}
if ($left_new) {
$inlines = array_merge(
$inlines,
$this->buildLintInlineComments($left));
}
if ($right_new) {
$inlines = array_merge(
$inlines,
$this->buildLintInlineComments($right));
}
$phids = array();
foreach ($inlines as $inline) {
$parser->parseInlineComment($inline);
if ($inline->getAuthorPHID()) {
$phids[$inline->getAuthorPHID()] = true;
}
}
$phids = array_keys($phids);
$handles = $this->loadViewerHandles($phids);
$parser->setHandles($handles);
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($viewer);
foreach ($inlines as $inline) {
$engine->addObject(
$inline,
PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY);
}
$engine->process();
$parser
->setUser($viewer)
->setMarkupEngine($engine)
->setShowEditAndReplyLinks(true)
->setCanMarkDone($can_mark)
->setObjectOwnerPHID($object_owner_phid)
->setRange($range_s, $range_e)
->setMask($mask);
if ($request->isAjax()) {
// NOTE: We must render the changeset before we render coverage
// information, since it builds some caches.
$rendered_changeset = $parser->renderChangeset();
$mcov = $parser->renderModifiedCoverage();
$coverage_data = array(
'differential-mcoverage-'.md5($changeset->getFilename()) => $mcov,
);
return id(new PhabricatorChangesetResponse())
->setRenderedChangeset($rendered_changeset)
->setCoverage($coverage_data)
->setUndoTemplates($parser->getRenderer()->renderUndoTemplates());
}
$detail = id(new DifferentialChangesetListView())
->setUser($this->getViewer())
->setChangesets(array($changeset))
->setVisibleChangesets(array($changeset))
->setRenderingReferences(array($rendering_reference))
->setRenderURI('/differential/changeset/')
->setDiff($diff)
->setTitle(pht('Standalone View'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setIsStandalone(true)
->setParser($parser);
if ($revision_id) {
$detail->setInlineCommentControllerURI(
'/differential/comment/inline/edit/'.$revision_id.'/');
}
$crumbs = $this->buildApplicationCrumbs();
if ($revision_id) {
$crumbs->addTextCrumb('D'.$revision_id, '/D'.$revision_id);
}
$diff_id = $diff->getID();
if ($diff_id) {
$crumbs->addTextCrumb(
pht('Diff %d', $diff_id),
$this->getApplicationURI('diff/'.$diff_id));
}
$crumbs->addTextCrumb($changeset->getDisplayFilename());
$crumbs->setBorder(true);
$header = id(new PHUIHeaderView())
->setHeader(pht('Changeset View'))
->setHeaderIcon('fa-gear');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter($detail);
return $this->newPage()
->setTitle(pht('Changeset View'))
->setCrumbs($crumbs)
->appendChild($view);
}
private function buildRawFileResponse(
DifferentialChangeset $changeset,
$is_new) {
$viewer = $this->getViewer();
if ($is_new) {
$key = 'raw:new:phid';
} else {
$key = 'raw:old:phid';
}
$metadata = $changeset->getMetadata();
$file = null;
$phid = idx($metadata, $key);
if ($phid) {
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->execute();
if ($file) {
$file = head($file);
}
}
if (!$file) {
// This is just building a cache of the changeset content in the file
// tool, and is safe to run on a read pathway.
$unguard = AphrontWriteGuard::beginScopedUnguardedWrites();
if ($is_new) {
$data = $changeset->makeNewFile();
} else {
$data = $changeset->makeOldFile();
}
$diff = $changeset->getDiff();
$file = PhabricatorFile::newFromFileData(
$data,
array(
'name' => $changeset->getFilename(),
'mime-type' => 'text/plain',
'ttl.relative' => phutil_units('24 hours in seconds'),
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
$file->attachToObject($diff->getPHID());
$metadata[$key] = $file->getPHID();
$changeset->setMetadata($metadata);
$changeset->save();
unset($unguard);
}
return $file->getRedirectResponse();
}
private function buildLintInlineComments($changeset) {
$diff = $changeset->getDiff();
$target_phids = $diff->getBuildTargetPHIDs();
if (!$target_phids) {
return array();
}
$messages = id(new HarbormasterBuildLintMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls) AND path = %s',
$target_phids,
$changeset->getFilename());
if (!$messages) {
return array();
}
$change_type = $changeset->getChangeType();
if (DifferentialChangeType::isDeleteChangeType($change_type)) {
// If this is a lint message on a deleted file, show it on the left
// side of the UI because there are no source code lines on the right
// side of the UI so inlines don't have anywhere to render. See PHI416.
$is_new = 0;
} else {
$is_new = 1;
}
$template = id(new DifferentialInlineComment())
->setChangesetID($changeset->getID())
->setIsNewFile($is_new)
->setLineLength(0);
$inlines = array();
foreach ($messages as $message) {
$description = $message->getProperty('description');
$inlines[] = id(clone $template)
->setSyntheticAuthor(pht('Lint: %s', $message->getName()))
->setLineNumber($message->getLine())
->setContent($description);
}
return $inlines;
}
private function loadCoverage(DifferentialChangeset $changeset) {
+ $viewer = $this->getViewer();
+
$target_phids = $changeset->getDiff()->getBuildTargetPHIDs();
if (!$target_phids) {
return null;
}
- $unit = id(new HarbormasterBuildUnitMessage())->loadAllWhere(
- 'buildTargetPHID IN (%Ls)',
- $target_phids);
-
+ $unit = id(new HarbormasterBuildUnitMessageQuery())
+ ->setViewer($viewer)
+ ->withBuildTargetPHIDs($target_phids)
+ ->execute();
if (!$unit) {
return null;
}
$coverage = array();
foreach ($unit as $message) {
$test_coverage = $message->getProperty('coverage');
if ($test_coverage === null) {
continue;
}
$coverage_data = idx($test_coverage, $changeset->getFileName());
if (!strlen($coverage_data)) {
continue;
}
$coverage[] = $coverage_data;
}
if (!$coverage) {
return null;
}
return ArcanistUnitTestResult::mergeCoverage($coverage);
}
}
diff --git a/src/applications/differential/controller/DifferentialController.php b/src/applications/differential/controller/DifferentialController.php
index 8fe5b5cac..334d46c3c 100644
--- a/src/applications/differential/controller/DifferentialController.php
+++ b/src/applications/differential/controller/DifferentialController.php
@@ -1,286 +1,287 @@
<?php
abstract class DifferentialController extends PhabricatorController {
private $packageChangesetMap;
private $pathPackageMap;
private $authorityPackages;
public function buildSideNavView($for_app = false) {
$viewer = $this->getRequest()->getUser();
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
id(new DifferentialRevisionSearchEngine())
->setViewer($viewer)
->addNavigationItems($nav->getMenu());
$nav->selectFilter(null);
return $nav;
}
public function buildApplicationMenu() {
return $this->buildSideNavView(true)->getMenu();
}
protected function buildPackageMaps(array $changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
$this->packageChangesetMap = array();
$this->pathPackageMap = array();
$this->authorityPackages = array();
if (!$changesets) {
return;
}
$viewer = $this->getViewer();
$have_owners = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorOwnersApplication',
$viewer);
if (!$have_owners) {
return;
}
$changeset = head($changesets);
$diff = $changeset->getDiff();
$repository_phid = $diff->getRepositoryPHID();
if (!$repository_phid) {
return;
}
if ($viewer->getPHID()) {
$packages = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
->withAuthorityPHIDs(array($viewer->getPHID()))
->execute();
$this->authorityPackages = $packages;
}
$paths = mpull($changesets, 'getOwnersFilename');
$control_query = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
->withControl($repository_phid, $paths);
$control_query->execute();
foreach ($changesets as $changeset) {
$changeset_path = $changeset->getOwnersFilename();
$packages = $control_query->getControllingPackagesForPath(
$repository_phid,
$changeset_path);
// If this particular changeset is generated code and the package does
// not match generated code, remove it from the list.
if ($changeset->isGeneratedChangeset()) {
foreach ($packages as $key => $package) {
if ($package->getMustMatchUngeneratedPaths()) {
unset($packages[$key]);
}
}
}
$this->pathPackageMap[$changeset_path] = $packages;
foreach ($packages as $package) {
$this->packageChangesetMap[$package->getPHID()][] = $changeset;
}
}
}
protected function getAuthorityPackages() {
if ($this->authorityPackages === null) {
throw new PhutilInvalidStateException('buildPackageMaps');
}
return $this->authorityPackages;
}
protected function getChangesetPackages(DifferentialChangeset $changeset) {
if ($this->pathPackageMap === null) {
throw new PhutilInvalidStateException('buildPackageMaps');
}
$path = $changeset->getOwnersFilename();
return idx($this->pathPackageMap, $path, array());
}
protected function getPackageChangesets($package_phid) {
if ($this->packageChangesetMap === null) {
throw new PhutilInvalidStateException('buildPackageMaps');
}
return idx($this->packageChangesetMap, $package_phid, array());
}
protected function buildTableOfContents(
array $changesets,
array $visible_changesets,
array $coverage) {
$viewer = $this->getViewer();
$toc_view = id(new PHUIDiffTableOfContentsListView())
->setViewer($viewer)
->setBare(true)
->setAuthorityPackages($this->getAuthorityPackages());
foreach ($changesets as $changeset_id => $changeset) {
$is_visible = isset($visible_changesets[$changeset_id]);
$anchor = $changeset->getAnchorName();
$filename = $changeset->getFilename();
$coverage_id = 'differential-mcoverage-'.md5($filename);
$item = id(new PHUIDiffTableOfContentsItemView())
->setChangeset($changeset)
->setIsVisible($is_visible)
->setAnchor($anchor)
->setCoverage(idx($coverage, $filename))
->setCoverageID($coverage_id);
$packages = $this->getChangesetPackages($changeset);
$item->setPackages($packages);
$toc_view->addItem($item);
}
return $toc_view;
}
protected function loadDiffProperties(array $diffs) {
$diffs = mpull($diffs, null, 'getID');
$properties = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID IN (%Ld)',
array_keys($diffs));
$properties = mgroup($properties, 'getDiffID');
foreach ($diffs as $id => $diff) {
$values = idx($properties, $id, array());
$values = mpull($values, 'getData', 'getName');
$diff->attachDiffProperties($values);
}
}
protected function loadHarbormasterData(array $diffs) {
$viewer = $this->getViewer();
$diffs = mpull($diffs, null, 'getPHID');
$buildables = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withBuildablePHIDs(array_keys($diffs))
->withManualBuildables(false)
->needBuilds(true)
->needTargets(true)
->execute();
$buildables = mpull($buildables, null, 'getBuildablePHID');
foreach ($diffs as $phid => $diff) {
$diff->attachBuildable(idx($buildables, $phid));
}
$target_map = array();
foreach ($diffs as $phid => $diff) {
$target_map[$phid] = $diff->getBuildTargetPHIDs();
}
$all_target_phids = array_mergev($target_map);
if ($all_target_phids) {
- $unit_messages = id(new HarbormasterBuildUnitMessage())->loadAllWhere(
- 'buildTargetPHID IN (%Ls)',
- $all_target_phids);
+ $unit_messages = id(new HarbormasterBuildUnitMessageQuery())
+ ->setViewer($viewer)
+ ->withBuildTargetPHIDs($all_target_phids)
+ ->execute();
$unit_messages = mgroup($unit_messages, 'getBuildTargetPHID');
} else {
$unit_messages = array();
}
foreach ($diffs as $phid => $diff) {
$target_phids = idx($target_map, $phid, array());
$messages = array_select_keys($unit_messages, $target_phids);
$messages = array_mergev($messages);
$diff->attachUnitMessages($messages);
}
// For diffs with no messages, look for legacy unit messages stored on the
// diff itself.
foreach ($diffs as $phid => $diff) {
if ($diff->getUnitMessages()) {
continue;
}
if (!$diff->hasDiffProperty('arc:unit')) {
continue;
}
$legacy_messages = $diff->getProperty('arc:unit');
if (!$legacy_messages) {
continue;
}
// Show the top 100 legacy lint messages. Previously, we showed some
// by default and let the user toggle the rest. With modern messages,
// we can send the user to the Harbormaster detail page. Just show
// "a lot" of messages in legacy cases to try to strike a balance
// between implementation simplicity and compatibility.
$legacy_messages = array_slice($legacy_messages, 0, 100);
$messages = array();
foreach ($legacy_messages as $message) {
$messages[] = HarbormasterBuildUnitMessage::newFromDictionary(
new HarbormasterBuildTarget(),
$this->getModernUnitMessageDictionary($message));
}
$diff->attachUnitMessages($messages);
}
}
private function getModernUnitMessageDictionary(array $map) {
// Strip out `null` values to satisfy stricter typechecks.
foreach ($map as $key => $value) {
if ($value === null) {
unset($map[$key]);
}
}
// Cast duration to a float since it used to be a string in some
// cases.
if (isset($map['duration'])) {
$map['duration'] = (double)$map['duration'];
}
return $map;
}
protected function getDiffTabLabels(array $diffs) {
// Make sure we're only going to render unique diffs.
$diffs = mpull($diffs, null, 'getID');
$labels = array(pht('Left'), pht('Right'));
$results = array();
foreach ($diffs as $diff) {
if (count($diffs) == 2) {
$label = array_shift($labels);
$label = pht('%s (Diff %d)', $label, $diff->getID());
} else {
$label = pht('Diff %d', $diff->getID());
}
$results[] = array(
$label,
$diff,
);
}
return $results;
}
}
diff --git a/src/applications/differential/controller/DifferentialDiffCreateController.php b/src/applications/differential/controller/DifferentialDiffCreateController.php
index 36f0b8620..9aaf407e2 100644
--- a/src/applications/differential/controller/DifferentialDiffCreateController.php
+++ b/src/applications/differential/controller/DifferentialDiffCreateController.php
@@ -1,212 +1,212 @@
<?php
final class DifferentialDiffCreateController extends DifferentialController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
// If we're on the "Update Diff" workflow, load the revision we're going
// to update.
$revision = null;
$revision_id = $request->getURIData('revisionID');
if ($revision_id) {
$revision = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($revision_id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$revision) {
return new Aphront404Response();
}
}
$diff = null;
// This object is just for policy stuff
$diff_object = DifferentialDiff::initializeNewDiff($viewer);
$repository_phid = null;
$errors = array();
$e_diff = null;
$e_file = null;
$validation_exception = null;
if ($request->isFormPost()) {
$repository_tokenizer = $request->getArr(
id(new DifferentialRepositoryField())->getFieldKey());
if ($repository_tokenizer) {
$repository_phid = reset($repository_tokenizer);
}
if ($request->getFileExists('diff-file')) {
$diff = PhabricatorFile::readUploadedFileData($_FILES['diff-file']);
} else {
$diff = $request->getStr('diff');
}
if (!strlen($diff)) {
$errors[] = pht(
'You can not create an empty diff. Paste a diff or upload a '.
'file containing a diff.');
$e_diff = pht('Required');
$e_file = pht('Required');
}
if (!$errors) {
try {
$call = new ConduitCall(
'differential.createrawdiff',
array(
'diff' => $diff,
'repositoryPHID' => $repository_phid,
'viewPolicy' => $request->getStr('viewPolicy'),
));
$call->setUser($viewer);
$result = $call->execute();
$diff_id = $result['id'];
$uri = $this->getApplicationURI("diff/{$diff_id}/");
$uri = new PhutilURI($uri);
if ($revision) {
- $uri->setQueryParam('revisionID', $revision->getID());
+ $uri->replaceQueryParam('revisionID', $revision->getID());
}
return id(new AphrontRedirectResponse())->setURI($uri);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
}
}
}
$form = new AphrontFormView();
$arcanist_href = PhabricatorEnv::getDoclink('Arcanist User Guide');
$arcanist_link = phutil_tag(
'a',
array(
'href' => $arcanist_href,
'target' => '_blank',
),
pht('Learn More'));
$cancel_uri = $this->getApplicationURI();
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->setObject($diff_object)
->execute();
$info_view = null;
if (!$request->isFormPost()) {
$info_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setErrors(
array(
array(
pht(
'The best way to create a diff is to use the Arcanist '.
'command-line tool.'),
' ',
$arcanist_link,
),
pht(
- 'You can also paste a diff below, or upload a file '.
+ 'You can also paste a diff above, or upload a file '.
'containing a diff (for example, from %s, %s or %s).',
phutil_tag('tt', array(), 'svn diff'),
phutil_tag('tt', array(), 'git diff'),
phutil_tag('tt', array(), 'hg diff --git')),
));
}
if ($revision) {
$title = pht('Update Diff');
$header = pht('Update Diff');
$button = pht('Continue');
$header_icon = 'fa-upload';
} else {
$title = pht('Create Diff');
$header = pht('Create New Diff');
$button = pht('Create Diff');
$header_icon = 'fa-plus-square';
}
$form
->setEncType('multipart/form-data')
->setUser($viewer);
if ($revision) {
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Updating Revision'))
->setValue($viewer->renderHandle($revision->getPHID())));
}
if ($repository_phid) {
$repository_value = array($repository_phid);
} else {
$repository_value = array();
}
$form
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel(pht('Raw Diff'))
->setName('diff')
->setValue($diff)
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
->setError($e_diff))
->appendChild(
id(new AphrontFormFileControl())
->setLabel(pht('Raw Diff From File'))
->setName('diff-file')
->setError($e_file))
->appendControl(
id(new AphrontFormTokenizerControl())
->setName(id(new DifferentialRepositoryField())->getFieldKey())
->setLabel(pht('Repository'))
->setDatasource(new DiffusionRepositoryDatasource())
->setValue($repository_value)
->setLimit(1))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($viewer)
->setName('viewPolicy')
->setPolicyObject($diff_object)
->setPolicies($policies)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($cancel_uri)
->setValue($button));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setValidationException($validation_exception)
->setForm($form)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->setFormErrors($errors);
$crumbs = $this->buildApplicationCrumbs();
if ($revision) {
$crumbs->addTextCrumb(
$revision->getMonogram(),
'/'.$revision->getMonogram());
}
$crumbs->addTextCrumb($title);
$crumbs->setBorder(true);
$view = id(new PHUITwoColumnView())
->setFooter(array(
$form_box,
$info_view,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
}
diff --git a/src/applications/differential/controller/DifferentialInlineCommentEditController.php b/src/applications/differential/controller/DifferentialInlineCommentEditController.php
index 9741cc93e..1de156a9b 100644
--- a/src/applications/differential/controller/DifferentialInlineCommentEditController.php
+++ b/src/applications/differential/controller/DifferentialInlineCommentEditController.php
@@ -1,235 +1,235 @@
<?php
final class DifferentialInlineCommentEditController
extends PhabricatorInlineCommentController {
private function getRevisionID() {
return $this->getRequest()->getURIData('id');
}
private function loadRevision() {
$viewer = $this->getViewer();
$revision_id = $this->getRevisionID();
$revision = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($revision_id))
->executeOne();
if (!$revision) {
throw new Exception(pht('Invalid revision ID "%s".', $revision_id));
}
return $revision;
}
protected function createComment() {
// Verify revision and changeset correspond to actual objects, and are
// connected to one another.
$changeset_id = $this->getChangesetID();
$viewer = $this->getViewer();
$revision = $this->loadRevision();
$changeset = id(new DifferentialChangesetQuery())
->setViewer($viewer)
->withIDs(array($changeset_id))
->executeOne();
if (!$changeset) {
throw new Exception(
pht(
'Invalid changeset ID "%s"!',
$changeset_id));
}
$diff = $changeset->getDiff();
if ($diff->getRevisionID() != $revision->getID()) {
throw new Exception(
pht(
'Changeset ID "%s" is part of diff ID "%s", but that diff '.
'is attached to revision "%s", not revision "%s".',
$changeset_id,
$diff->getID(),
$diff->getRevisionID(),
$revision->getID()));
}
return id(new DifferentialInlineComment())
->setRevision($revision)
->setChangesetID($changeset_id);
}
protected function loadComment($id) {
return id(new DifferentialInlineCommentQuery())
->setViewer($this->getViewer())
->withIDs(array($id))
->withDeletedDrafts(true)
->needHidden(true)
->executeOne();
}
protected function loadCommentByPHID($phid) {
return id(new DifferentialInlineCommentQuery())
->setViewer($this->getViewer())
->withPHIDs(array($phid))
->withDeletedDrafts(true)
->needHidden(true)
->executeOne();
}
protected function loadCommentForEdit($id) {
$viewer = $this->getViewer();
$inline = $this->loadComment($id);
if (!$this->canEditInlineComment($viewer, $inline)) {
throw new Exception(pht('That comment is not editable!'));
}
return $inline;
}
protected function loadCommentForDone($id) {
$viewer = $this->getViewer();
$inline = $this->loadComment($id);
if (!$inline) {
throw new Exception(pht('Unable to load inline "%d".', $id));
}
$changeset = id(new DifferentialChangesetQuery())
->setViewer($viewer)
->withIDs(array($inline->getChangesetID()))
->executeOne();
if (!$changeset) {
throw new Exception(pht('Unable to load changeset.'));
}
$diff = id(new DifferentialDiffQuery())
->setViewer($viewer)
->withIDs(array($changeset->getDiffID()))
->executeOne();
if (!$diff) {
throw new Exception(pht('Unable to load diff.'));
}
$revision = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($diff->getRevisionID()))
->executeOne();
if (!$revision) {
throw new Exception(pht('Unable to load revision.'));
}
$viewer_phid = $viewer->getPHID();
$is_owner = ($viewer_phid == $revision->getAuthorPHID());
$is_author = ($viewer_phid == $inline->getAuthorPHID());
$is_draft = ($inline->isDraft());
if ($is_owner) {
// You own the revision, so you can mark the comment as "Done".
} else if ($is_author && $is_draft) {
// You made this comment and it's still a draft, so you can mark
// it as "Done".
} else {
throw new Exception(
pht(
'You are not the revision owner, and this is not a draft comment '.
'you authored.'));
}
return $inline;
}
private function canEditInlineComment(
PhabricatorUser $viewer,
DifferentialInlineComment $inline) {
// Only the author may edit a comment.
if ($inline->getAuthorPHID() != $viewer->getPHID()) {
return false;
}
// Saved comments may not be edited, for now, although the schema now
// supports it.
if (!$inline->isDraft()) {
return false;
}
// Inline must be attached to the active revision.
if ($inline->getRevisionID() != $this->getRevisionID()) {
return false;
}
return true;
}
protected function deleteComment(PhabricatorInlineCommentInterface $inline) {
$inline->openTransaction();
$inline->setIsDeleted(1)->save();
$this->syncDraft();
$inline->saveTransaction();
}
protected function undeleteComment(
PhabricatorInlineCommentInterface $inline) {
$inline->openTransaction();
$inline->setIsDeleted(0)->save();
$this->syncDraft();
$inline->saveTransaction();
}
protected function saveComment(PhabricatorInlineCommentInterface $inline) {
$inline->openTransaction();
$inline->save();
$this->syncDraft();
$inline->saveTransaction();
}
protected function loadObjectOwnerPHID(
PhabricatorInlineCommentInterface $inline) {
return $this->loadRevision()->getAuthorPHID();
}
protected function hideComments(array $ids) {
$viewer = $this->getViewer();
$table = new DifferentialHiddenComment();
$conn_w = $table->establishConnection('w');
$sql = array();
foreach ($ids as $id) {
$sql[] = qsprintf(
$conn_w,
'(%s, %d)',
$viewer->getPHID(),
$id);
}
queryfx(
$conn_w,
- 'INSERT IGNORE INTO %T (userPHID, commentID) VALUES %Q',
+ 'INSERT IGNORE INTO %T (userPHID, commentID) VALUES %LQ',
$table->getTableName(),
- implode(', ', $sql));
+ $sql);
}
protected function showComments(array $ids) {
$viewer = $this->getViewer();
$table = new DifferentialHiddenComment();
$conn_w = $table->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE userPHID = %s AND commentID IN (%Ld)',
$table->getTableName(),
$viewer->getPHID(),
$ids);
}
private function syncDraft() {
$viewer = $this->getViewer();
$revision = $this->loadRevision();
$revision->newDraftEngine()
->setObject($revision)
->setViewer($viewer)
->synchronize();
}
}
diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php
index 8216c1155..ba36b1da1 100644
--- a/src/applications/differential/controller/DifferentialRevisionViewController.php
+++ b/src/applications/differential/controller/DifferentialRevisionViewController.php
@@ -1,1455 +1,1448 @@
<?php
final class DifferentialRevisionViewController
extends DifferentialController {
private $revisionID;
private $changesetCount;
private $hiddenChangesets;
private $warnings = array();
public function shouldAllowPublic() {
return true;
}
public function isLargeDiff() {
return ($this->getChangesetCount() > $this->getLargeDiffLimit());
}
public function isVeryLargeDiff() {
return ($this->getChangesetCount() > $this->getVeryLargeDiffLimit());
}
public function getLargeDiffLimit() {
return 100;
}
public function getVeryLargeDiffLimit() {
return 1000;
}
public function getChangesetCount() {
if ($this->changesetCount === null) {
throw new PhutilInvalidStateException('setChangesetCount');
}
return $this->changesetCount;
}
public function setChangesetCount($count) {
$this->changesetCount = $count;
return $this;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$this->revisionID = $request->getURIData('id');
$viewer_is_anonymous = !$viewer->isLoggedIn();
$revision = id(new DifferentialRevisionQuery())
->withIDs(array($this->revisionID))
->setViewer($viewer)
->needReviewers(true)
->needReviewerAuthority(true)
->executeOne();
if (!$revision) {
return new Aphront404Response();
}
$diffs = id(new DifferentialDiffQuery())
->setViewer($viewer)
->withRevisionIDs(array($this->revisionID))
->execute();
$diffs = array_reverse($diffs, $preserve_keys = true);
if (!$diffs) {
throw new Exception(
pht('This revision has no diffs. Something has gone quite wrong.'));
}
$revision->attachActiveDiff(last($diffs));
$diff_vs = $this->getOldDiffID($revision, $diffs);
if ($diff_vs instanceof AphrontResponse) {
return $diff_vs;
}
$target_id = $this->getNewDiffID($revision, $diffs);
if ($target_id instanceof AphrontResponse) {
return $target_id;
}
$target = $diffs[$target_id];
$target_manual = $target;
if (!$target_id) {
foreach ($diffs as $diff) {
if ($diff->getCreationMethod() != 'commit') {
$target_manual = $diff;
}
}
}
$repository = null;
$repository_phid = $target->getRepositoryPHID();
if ($repository_phid) {
if ($repository_phid == $revision->getRepositoryPHID()) {
$repository = $revision->getRepository();
} else {
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withPHIDs(array($repository_phid))
->executeOne();
}
}
list($changesets, $vs_map, $vs_changesets, $rendering_references) =
$this->loadChangesetsAndVsMap(
$target,
idx($diffs, $diff_vs),
$repository);
$this->setChangesetCount(count($rendering_references));
if ($request->getExists('download')) {
return $this->buildRawDiffResponse(
$revision,
$changesets,
$vs_changesets,
$vs_map,
$repository);
}
$map = $vs_map;
if (!$map) {
$map = array_fill_keys(array_keys($changesets), 0);
}
$old_ids = array();
$new_ids = array();
foreach ($map as $id => $vs) {
if ($vs <= 0) {
$old_ids[] = $id;
$new_ids[] = $id;
} else {
$new_ids[] = $id;
$new_ids[] = $vs;
}
}
$this->loadDiffProperties($diffs);
$props = $target_manual->getDiffProperties();
$subscriber_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID(
$revision->getPHID());
$object_phids = array_merge(
$revision->getReviewerPHIDs(),
$subscriber_phids,
$revision->loadCommitPHIDs(),
array(
$revision->getAuthorPHID(),
$viewer->getPHID(),
));
foreach ($revision->getAttached() as $type => $phids) {
foreach ($phids as $phid => $info) {
$object_phids[] = $phid;
}
}
$field_list = PhabricatorCustomField::getObjectFields(
$revision,
PhabricatorCustomField::ROLE_VIEW);
$field_list->setViewer($viewer);
$field_list->readFieldsFromStorage($revision);
$warning_handle_map = array();
foreach ($field_list->getFields() as $key => $field) {
$req = $field->getRequiredHandlePHIDsForRevisionHeaderWarnings();
foreach ($req as $phid) {
$warning_handle_map[$key][] = $phid;
$object_phids[] = $phid;
}
}
$handles = $this->loadViewerHandles($object_phids);
$warnings = $this->warnings;
$request_uri = $request->getRequestURI();
$large = $request->getStr('large');
$large_warning =
($this->isLargeDiff()) &&
(!$this->isVeryLargeDiff()) &&
(!$large);
if ($large_warning) {
$count = $this->getChangesetCount();
$expand_uri = $request_uri
->alter('large', 'true')
->setFragment('toc');
$message = array(
pht(
'This large diff affects %s files. Files without inline '.
'comments have been collapsed.',
new PhutilNumber($count)),
' ',
phutil_tag(
'strong',
array(),
phutil_tag(
'a',
array(
'href' => $expand_uri,
),
pht('Expand All Files'))),
);
$warnings[] = id(new PHUIInfoView())
->setTitle(pht('Large Diff'))
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->appendChild($message);
$folded_changesets = $changesets;
} else {
$folded_changesets = array();
}
// Don't hide or fold changesets which have inline comments.
$hidden_changesets = $this->hiddenChangesets;
if ($hidden_changesets || $folded_changesets) {
$old = array_select_keys($changesets, $old_ids);
$new = array_select_keys($changesets, $new_ids);
$query = id(new DifferentialInlineCommentQuery())
->setViewer($viewer)
->needHidden(true)
->withRevisionPHIDs(array($revision->getPHID()));
$inlines = $query->execute();
$inlines = $query->adjustInlinesForChangesets(
$inlines,
$old,
$new,
$revision);
foreach ($inlines as $inline) {
$changeset_id = $inline->getChangesetID();
if (!isset($changesets[$changeset_id])) {
continue;
}
unset($hidden_changesets[$changeset_id]);
unset($folded_changesets[$changeset_id]);
}
}
// If we would hide only one changeset, don't hide anything. The notice
// we'd render about it is about the same size as the changeset.
if (count($hidden_changesets) < 2) {
$hidden_changesets = array();
}
// Update the set of hidden changesets, since we may have just un-hidden
// some of them.
if ($hidden_changesets) {
$warnings[] = id(new PHUIInfoView())
->setTitle(pht('Showing Only Differences'))
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->appendChild(
pht(
'This revision modifies %s more files that are hidden because '.
'they were not modified between selected diffs and they have no '.
'inline comments.',
phutil_count($hidden_changesets)));
}
// Compute the unfolded changesets. By default, everything is unfolded.
$unfolded_changesets = $changesets;
foreach ($folded_changesets as $changeset_id => $changeset) {
unset($unfolded_changesets[$changeset_id]);
}
// Throw away any hidden changesets.
foreach ($hidden_changesets as $changeset_id => $changeset) {
unset($changesets[$changeset_id]);
unset($unfolded_changesets[$changeset_id]);
}
$commit_hashes = mpull($diffs, 'getSourceControlBaseRevision');
$local_commits = idx($props, 'local:commits', array());
foreach ($local_commits as $local_commit) {
$commit_hashes[] = idx($local_commit, 'tree');
$commit_hashes[] = idx($local_commit, 'local');
}
$commit_hashes = array_unique(array_filter($commit_hashes));
if ($commit_hashes) {
$commits_for_links = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withIdentifiers($commit_hashes)
->execute();
$commits_for_links = mpull(
$commits_for_links,
null,
'getCommitIdentifier');
} else {
$commits_for_links = array();
}
$header = $this->buildHeader($revision);
$subheader = $this->buildSubheaderView($revision);
$details = $this->buildDetails($revision, $field_list);
$curtain = $this->buildCurtain($revision);
- $whitespace = $request->getStr(
- 'whitespace',
- DifferentialChangesetParser::WHITESPACE_IGNORE_MOST);
-
$repository = $revision->getRepository();
if ($repository) {
$symbol_indexes = $this->buildSymbolIndexes(
$repository,
$unfolded_changesets);
} else {
$symbol_indexes = array();
}
$revision_warnings = $this->buildRevisionWarnings(
$revision,
$field_list,
$warning_handle_map,
$handles);
$info_view = null;
if ($revision_warnings) {
$info_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors($revision_warnings);
}
$detail_diffs = array_select_keys(
$diffs,
array($diff_vs, $target->getID()));
$detail_diffs = mpull($detail_diffs, null, 'getPHID');
$this->loadHarbormasterData($detail_diffs);
$diff_detail_box = $this->buildDiffDetailView(
$detail_diffs,
$revision,
$field_list);
$unit_box = $this->buildUnitMessagesView(
$target,
$revision);
$timeline = $this->buildTransactions(
$revision,
$diff_vs ? $diffs[$diff_vs] : $target,
$target,
$old_ids,
$new_ids);
$timeline->setQuoteRef($revision->getMonogram());
if ($this->isVeryLargeDiff()) {
$messages = array();
$messages[] = pht(
'This very large diff affects more than %s files. Use the %s to '.
'browse changes.',
new PhutilNumber($this->getVeryLargeDiffLimit()),
phutil_tag(
'a',
array(
'href' => '/differential/diff/'.$target->getID().'/changesets/',
),
phutil_tag('strong', array(), pht('Changeset List'))));
$changeset_view = id(new PHUIInfoView())
->setErrors($messages);
} else {
$changeset_view = id(new DifferentialChangesetListView())
->setChangesets($changesets)
->setVisibleChangesets($unfolded_changesets)
->setStandaloneURI('/differential/changeset/')
->setRawFileURIs(
'/differential/changeset/?view=old',
'/differential/changeset/?view=new')
->setUser($viewer)
->setDiff($target)
->setRenderingReferences($rendering_references)
->setVsMap($vs_map)
- ->setWhitespace($whitespace)
->setSymbolIndexes($symbol_indexes)
->setTitle(pht('Diff %s', $target->getID()))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
$revision_id = $revision->getID();
$inline_list_uri = "/revision/inlines/{$revision_id}/";
$inline_list_uri = $this->getApplicationURI($inline_list_uri);
$changeset_view->setInlineListURI($inline_list_uri);
if ($repository) {
$changeset_view->setRepository($repository);
}
if (!$viewer_is_anonymous) {
$changeset_view->setInlineCommentControllerURI(
'/differential/comment/inline/edit/'.$revision->getID().'/');
}
}
$broken_diffs = $this->loadHistoryDiffStatus($diffs);
$history = id(new DifferentialRevisionUpdateHistoryView())
->setUser($viewer)
->setDiffs($diffs)
->setDiffUnitStatuses($broken_diffs)
->setSelectedVersusDiffID($diff_vs)
->setSelectedDiffID($target->getID())
- ->setSelectedWhitespace($whitespace)
->setCommitsForLinks($commits_for_links);
$local_table = id(new DifferentialLocalCommitsView())
->setUser($viewer)
->setLocalCommits(idx($props, 'local:commits'))
->setCommitsForLinks($commits_for_links);
if ($repository && !$this->isVeryLargeDiff()) {
$other_revisions = $this->loadOtherRevisions(
$changesets,
$target,
$repository);
} else {
$other_revisions = array();
}
$other_view = null;
if ($other_revisions) {
$other_view = $this->renderOtherRevisions($other_revisions);
}
if ($this->isVeryLargeDiff()) {
$toc_view = null;
// When rendering a "very large" diff, we skip computation of owners
// that own no files because it is significantly expensive and not very
// valuable.
foreach ($revision->getReviewers() as $reviewer) {
// Give each reviewer a dummy nonempty value so the UI does not render
// the "(Owns No Changed Paths)" note. If that behavior becomes more
// sophisticated in the future, this behavior might also need to.
$reviewer->attachChangesets($changesets);
}
} else {
$this->buildPackageMaps($changesets);
$toc_view = $this->buildTableOfContents(
$changesets,
$unfolded_changesets,
$target->loadCoverageMap($viewer));
// Attach changesets to each reviewer so we can show which Owners package
// reviewers own no files.
foreach ($revision->getReviewers() as $reviewer) {
$reviewer_phid = $reviewer->getReviewerPHID();
$reviewer_changesets = $this->getPackageChangesets($reviewer_phid);
$reviewer->attachChangesets($reviewer_changesets);
}
}
$tab_group = new PHUITabGroupView();
if ($toc_view) {
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('Files'))
->setKey('files')
->appendChild($toc_view));
}
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('History'))
->setKey('history')
->appendChild($history));
$filetree_on = $viewer->compareUserSetting(
PhabricatorShowFiletreeSetting::SETTINGKEY,
PhabricatorShowFiletreeSetting::VALUE_ENABLE_FILETREE);
$collapsed_key = PhabricatorFiletreeVisibleSetting::SETTINGKEY;
$filetree_collapsed = (bool)$viewer->getUserSetting($collapsed_key);
// See PHI811. If the viewer has the file tree on, the files tab with the
// table of contents is redundant, so default to the "History" tab instead.
if ($filetree_on && !$filetree_collapsed) {
$tab_group->selectTab('history');
}
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('Commits'))
->setKey('commits')
->appendChild($local_table));
$stack_graph = id(new DifferentialRevisionGraph())
->setViewer($viewer)
->setSeedPHID($revision->getPHID())
->setLoadEntireGraph(true)
->loadGraph();
if (!$stack_graph->isEmpty()) {
$stack_table = $stack_graph->newGraphTable();
$parent_type = DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST;
$reachable = $stack_graph->getReachableObjects($parent_type);
foreach ($reachable as $key => $reachable_revision) {
if ($reachable_revision->isClosed()) {
unset($reachable[$key]);
}
}
if ($reachable) {
$stack_name = pht('Stack (%s Open)', phutil_count($reachable));
$stack_color = PHUIListItemView::STATUS_FAIL;
} else {
$stack_name = pht('Stack');
$stack_color = null;
}
$tab_group->addTab(
id(new PHUITabView())
->setName($stack_name)
->setKey('stack')
->setColor($stack_color)
->appendChild($stack_table));
}
if ($other_view) {
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('Similar'))
->setKey('similar')
->appendChild($other_view));
}
$view_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Changeset List'))
->setHref('/differential/diff/'.$target->getID().'/changesets/')
->setIcon('fa-align-left');
$tab_header = id(new PHUIHeaderView())
->setHeader(pht('Revision Contents'))
->addActionLink($view_button);
$tab_view = id(new PHUIObjectBoxView())
->setHeader($tab_header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addTabGroup($tab_group);
$signatures = DifferentialRequiredSignaturesField::loadForRevision(
$revision);
$missing_signatures = false;
foreach ($signatures as $phid => $signed) {
if (!$signed) {
$missing_signatures = true;
}
}
$footer = array();
$signature_message = null;
if ($missing_signatures) {
$signature_message = id(new PHUIInfoView())
->setTitle(pht('Content Hidden'))
->appendChild(
pht(
'The content of this revision is hidden until the author has '.
'signed all of the required legal agreements.'));
} else {
$anchor = id(new PhabricatorAnchorView())
->setAnchorName('toc')
->setNavigationMarker(true);
$footer[] = array(
$anchor,
$warnings,
$tab_view,
$changeset_view,
);
}
$comment_view = id(new DifferentialRevisionEditEngine())
->setViewer($viewer)
->buildEditEngineCommentView($revision);
$comment_view->setTransactionTimeline($timeline);
$review_warnings = array();
foreach ($field_list->getFields() as $field) {
$review_warnings[] = $field->getWarningsForDetailView();
}
$review_warnings = array_mergev($review_warnings);
if ($review_warnings) {
$warnings_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors($review_warnings);
$comment_view->setInfoView($warnings_view);
}
$footer[] = $comment_view;
$monogram = $revision->getMonogram();
$operations_box = $this->buildOperationsBox($revision);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($monogram);
$crumbs->setBorder(true);
$nav = null;
if ($filetree_on && !$this->isVeryLargeDiff()) {
$width_key = PhabricatorFiletreeWidthSetting::SETTINGKEY;
$width_value = $viewer->getUserSetting($width_key);
$nav = id(new DifferentialChangesetFileTreeSideNavBuilder())
->setTitle($monogram)
->setBaseURI(new PhutilURI($revision->getURI()))
->setCollapsed($filetree_collapsed)
->setWidth((int)$width_value)
->build($changesets);
}
- Javelin::initBehavior('differential-user-select');
-
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setSubheader($subheader)
->setCurtain($curtain)
->setMainColumn(
array(
$operations_box,
$info_view,
$details,
$diff_detail_box,
$unit_box,
$timeline,
$signature_message,
))
->setFooter($footer);
$page = $this->newPage()
->setTitle($monogram.' '.$revision->getTitle())
->setCrumbs($crumbs)
->setPageObjectPHIDs(array($revision->getPHID()))
->appendChild($view);
if ($nav) {
$page->setNavigation($nav);
}
return $page;
}
private function buildHeader(DifferentialRevision $revision) {
$view = id(new PHUIHeaderView())
->setHeader($revision->getTitle($revision))
->setUser($this->getViewer())
->setPolicyObject($revision)
->setHeaderIcon('fa-cog');
$status_tag = id(new PHUITagView())
->setName($revision->getStatusDisplayName())
->setIcon($revision->getStatusIcon())
->setColor($revision->getStatusTagColor())
->setType(PHUITagView::TYPE_SHADE);
$view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_tag);
// If the revision is in a status other than "Draft", but not broadcasting,
// add an additional "Draft" tag to the header to make it clear that this
// revision hasn't promoted yet.
if (!$revision->getShouldBroadcast() && !$revision->isDraft()) {
$draft_status = DifferentialRevisionStatus::newForStatus(
DifferentialRevisionStatus::DRAFT);
$draft_tag = id(new PHUITagView())
->setName($draft_status->getDisplayName())
->setIcon($draft_status->getIcon())
->setColor($draft_status->getTagColor())
->setType(PHUITagView::TYPE_SHADE);
$view->addTag($draft_tag);
}
return $view;
}
private function buildSubheaderView(DifferentialRevision $revision) {
$viewer = $this->getViewer();
$author_phid = $revision->getAuthorPHID();
$author = $viewer->renderHandle($author_phid)->render();
$date = phabricator_datetime($revision->getDateCreated(), $viewer);
$author = phutil_tag('strong', array(), $author);
$handles = $viewer->loadHandles(array($author_phid));
$image_uri = $handles[$author_phid]->getImageURI();
$image_href = $handles[$author_phid]->getURI();
$content = pht('Authored by %s on %s.', $author, $date);
return id(new PHUIHeadThingView())
->setImage($image_uri)
->setImageHref($image_href)
->setContent($content);
}
private function buildDetails(
DifferentialRevision $revision,
$custom_fields) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer);
if ($custom_fields) {
$custom_fields->appendFieldsToPropertyList(
$revision,
$viewer,
$properties);
}
$header = id(new PHUIHeaderView())
->setHeader(pht('Details'));
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($properties);
}
private function buildCurtain(DifferentialRevision $revision) {
$viewer = $this->getViewer();
$revision_id = $revision->getID();
$revision_phid = $revision->getPHID();
$curtain = $this->newCurtainView($revision);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$revision,
PhabricatorPolicyCapability::CAN_EDIT);
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setHref("/differential/revision/edit/{$revision_id}/")
->setName(pht('Edit Revision'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-upload')
->setHref("/differential/revision/update/{$revision_id}/")
->setName(pht('Update Diff'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$request_uri = $this->getRequest()->getRequestURI();
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-download')
->setName(pht('Download Raw Diff'))
->setHref($request_uri->alter('download', 'true')));
$relationship_list = PhabricatorObjectRelationshipList::newForObject(
$viewer,
$revision);
$revision_actions = array(
DifferentialRevisionHasParentRelationship::RELATIONSHIPKEY,
DifferentialRevisionHasChildRelationship::RELATIONSHIPKEY,
);
$revision_submenu = $relationship_list->newActionSubmenu($revision_actions)
->setName(pht('Edit Related Revisions...'))
->setIcon('fa-cog');
$curtain->addAction($revision_submenu);
$relationship_submenu = $relationship_list->newActionMenu();
if ($relationship_submenu) {
$curtain->addAction($relationship_submenu);
}
$repository = $revision->getRepository();
if ($repository && $repository->canPerformAutomation()) {
$revision_id = $revision->getID();
$op = new DrydockLandRepositoryOperation();
$barrier = $op->getBarrierToLanding($viewer, $revision);
if ($barrier) {
$can_land = false;
} else {
$can_land = true;
}
$action = id(new PhabricatorActionView())
->setName(pht('Land Revision'))
->setIcon('fa-fighter-jet')
->setHref("/differential/revision/operation/{$revision_id}/")
->setWorkflow(true)
->setDisabled(!$can_land);
$curtain->addAction($action);
}
return $curtain;
}
private function loadHistoryDiffStatus(array $diffs) {
assert_instances_of($diffs, 'DifferentialDiff');
$diff_phids = mpull($diffs, 'getPHID');
$bad_unit_status = array(
ArcanistUnitTestResult::RESULT_FAIL,
ArcanistUnitTestResult::RESULT_BROKEN,
);
$message = new HarbormasterBuildUnitMessage();
$target = new HarbormasterBuildTarget();
$build = new HarbormasterBuild();
$buildable = new HarbormasterBuildable();
$broken_diffs = queryfx_all(
$message->establishConnection('r'),
'SELECT distinct a.buildablePHID
FROM %T m
JOIN %T t ON m.buildTargetPHID = t.phid
JOIN %T b ON t.buildPHID = b.phid
JOIN %T a ON b.buildablePHID = a.phid
WHERE a.buildablePHID IN (%Ls)
AND m.result in (%Ls)',
$message->getTableName(),
$target->getTableName(),
$build->getTableName(),
$buildable->getTableName(),
$diff_phids,
$bad_unit_status);
$unit_status = array();
foreach ($broken_diffs as $broken) {
$phid = $broken['buildablePHID'];
$unit_status[$phid] = DifferentialUnitStatus::UNIT_FAIL;
}
return $unit_status;
}
private function loadChangesetsAndVsMap(
DifferentialDiff $target,
DifferentialDiff $diff_vs = null,
PhabricatorRepository $repository = null) {
$viewer = $this->getViewer();
$load_diffs = array($target);
if ($diff_vs) {
$load_diffs[] = $diff_vs;
}
$raw_changesets = id(new DifferentialChangesetQuery())
->setViewer($viewer)
->withDiffs($load_diffs)
->execute();
$changeset_groups = mgroup($raw_changesets, 'getDiffID');
$changesets = idx($changeset_groups, $target->getID(), array());
$changesets = mpull($changesets, null, 'getID');
$refs = array();
$vs_map = array();
$vs_changesets = array();
$must_compare = array();
if ($diff_vs) {
$vs_id = $diff_vs->getID();
$vs_changesets_path_map = array();
foreach (idx($changeset_groups, $vs_id, array()) as $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $diff_vs);
$vs_changesets_path_map[$path] = $changeset;
$vs_changesets[$changeset->getID()] = $changeset;
}
foreach ($changesets as $key => $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $target);
if (isset($vs_changesets_path_map[$path])) {
$vs_map[$changeset->getID()] =
$vs_changesets_path_map[$path]->getID();
$refs[$changeset->getID()] =
$changeset->getID().'/'.$vs_changesets_path_map[$path]->getID();
unset($vs_changesets_path_map[$path]);
$must_compare[] = $changeset->getID();
} else {
$refs[$changeset->getID()] = $changeset->getID();
}
}
foreach ($vs_changesets_path_map as $path => $changeset) {
$changesets[$changeset->getID()] = $changeset;
$vs_map[$changeset->getID()] = -1;
$refs[$changeset->getID()] = $changeset->getID().'/-1';
}
} else {
foreach ($changesets as $changeset) {
$refs[$changeset->getID()] = $changeset->getID();
}
}
$changesets = msort($changesets, 'getSortKey');
// See T13137. When displaying the diff between two updates, hide any
// changesets which haven't actually changed.
$this->hiddenChangesets = array();
foreach ($must_compare as $changeset_id) {
$changeset = $changesets[$changeset_id];
$vs_changeset = $vs_changesets[$vs_map[$changeset_id]];
if ($changeset->hasSameEffectAs($vs_changeset)) {
$this->hiddenChangesets[$changeset_id] = $changesets[$changeset_id];
}
}
return array($changesets, $vs_map, $vs_changesets, $refs);
}
private function buildSymbolIndexes(
PhabricatorRepository $repository,
array $unfolded_changesets) {
assert_instances_of($unfolded_changesets, 'DifferentialChangeset');
$engine = PhabricatorSyntaxHighlighter::newEngine();
$langs = $repository->getSymbolLanguages();
$langs = nonempty($langs, array());
$sources = $repository->getSymbolSources();
$sources = nonempty($sources, array());
$symbol_indexes = array();
if ($langs && $sources) {
$have_symbols = id(new DiffusionSymbolQuery())
->existsSymbolsInRepository($repository->getPHID());
if (!$have_symbols) {
return $symbol_indexes;
}
}
$repository_phids = array_merge(
array($repository->getPHID()),
$sources);
$indexed_langs = array_fill_keys($langs, true);
foreach ($unfolded_changesets as $key => $changeset) {
$lang = $engine->getLanguageFromFilename($changeset->getFilename());
if (empty($indexed_langs) || isset($indexed_langs[$lang])) {
$symbol_indexes[$key] = array(
'lang' => $lang,
'repositories' => $repository_phids,
);
}
}
return $symbol_indexes;
}
private function loadOtherRevisions(
array $changesets,
DifferentialDiff $target,
PhabricatorRepository $repository) {
assert_instances_of($changesets, 'DifferentialChangeset');
$paths = array();
foreach ($changesets as $changeset) {
$paths[] = $changeset->getAbsoluteRepositoryPath(
$repository,
$target);
}
if (!$paths) {
return array();
}
$path_map = id(new DiffusionPathIDQuery($paths))->loadPathIDs();
if (!$path_map) {
return array();
}
$recent = (PhabricatorTime::getNow() - phutil_units('30 days in seconds'));
$query = id(new DifferentialRevisionQuery())
->setViewer($this->getRequest()->getUser())
->withIsOpen(true)
->withUpdatedEpochBetween($recent, null)
->setOrder(DifferentialRevisionQuery::ORDER_MODIFIED)
->setLimit(10)
->needFlags(true)
->needDrafts(true)
->needReviewers(true);
foreach ($path_map as $path => $path_id) {
$query->withPath($repository->getID(), $path_id);
}
$results = $query->execute();
// Strip out *this* revision.
foreach ($results as $key => $result) {
if ($result->getID() == $this->revisionID) {
unset($results[$key]);
}
}
return $results;
}
private function renderOtherRevisions(array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$viewer = $this->getViewer();
$header = id(new PHUIHeaderView())
->setHeader(pht('Recent Similar Revisions'));
return id(new DifferentialRevisionListView())
->setViewer($viewer)
->setRevisions($revisions)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setNoBox(true);
}
/**
* Note this code is somewhat similar to the buildPatch method in
* @{class:DifferentialReviewRequestMail}.
*
* @return @{class:AphrontRedirectResponse}
*/
private function buildRawDiffResponse(
DifferentialRevision $revision,
array $changesets,
array $vs_changesets,
array $vs_map,
PhabricatorRepository $repository = null) {
assert_instances_of($changesets, 'DifferentialChangeset');
assert_instances_of($vs_changesets, 'DifferentialChangeset');
$viewer = $this->getViewer();
id(new DifferentialHunkQuery())
->setViewer($viewer)
->withChangesets($changesets)
->needAttachToChangesets(true)
->execute();
$diff = new DifferentialDiff();
$diff->attachChangesets($changesets);
$raw_changes = $diff->buildChangesList();
$changes = array();
foreach ($raw_changes as $changedict) {
$changes[] = ArcanistDiffChange::newFromDictionary($changedict);
}
$loader = id(new PhabricatorFileBundleLoader())
->setViewer($viewer);
$bundle = ArcanistBundle::newFromChanges($changes);
$bundle->setLoadFileDataCallback(array($loader, 'loadFileData'));
$vcs = $repository ? $repository->getVersionControlSystem() : null;
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$raw_diff = $bundle->toGitPatch();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
default:
$raw_diff = $bundle->toUnifiedDiff();
break;
}
$request_uri = $this->getRequest()->getRequestURI();
// this ends up being something like
// D123.diff
// or the verbose
- // D123.vs123.id123.whitespaceignore-all.diff
+ // D123.vs123.id123.highlightjs.diff
// lame but nice to include these options
$file_name = ltrim($request_uri->getPath(), '/').'.';
- foreach ($request_uri->getQueryParams() as $key => $value) {
+ foreach ($request_uri->getQueryParamsAsPairList() as $pair) {
+ list($key, $value) = $pair;
if ($key == 'download') {
continue;
}
$file_name .= $key.$value.'.';
}
$file_name .= 'diff';
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = PhabricatorFile::newFromFileData(
$raw_diff,
array(
'name' => $file_name,
'ttl.relative' => phutil_units('24 hours in seconds'),
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
$file->attachToObject($revision->getPHID());
unset($unguarded);
return $file->getRedirectResponse();
}
private function buildTransactions(
DifferentialRevision $revision,
DifferentialDiff $left_diff,
DifferentialDiff $right_diff,
array $old_ids,
array $new_ids) {
$timeline = $this->buildTransactionTimeline(
$revision,
new DifferentialTransactionQuery(),
$engine = null,
array(
'left' => $left_diff->getID(),
'right' => $right_diff->getID(),
'old' => implode(',', $old_ids),
'new' => implode(',', $new_ids),
));
return $timeline;
}
private function buildRevisionWarnings(
DifferentialRevision $revision,
PhabricatorCustomFieldList $field_list,
array $warning_handle_map,
array $handles) {
$warnings = array();
foreach ($field_list->getFields() as $key => $field) {
$phids = idx($warning_handle_map, $key, array());
$field_handles = array_select_keys($handles, $phids);
$field_warnings = $field->getWarningsForRevisionHeader($field_handles);
foreach ($field_warnings as $warning) {
$warnings[] = $warning;
}
}
return $warnings;
}
private function buildDiffDetailView(
array $diffs,
DifferentialRevision $revision,
PhabricatorCustomFieldList $field_list) {
$viewer = $this->getViewer();
$fields = array();
foreach ($field_list->getFields() as $field) {
if ($field->shouldAppearInDiffPropertyView()) {
$fields[] = $field;
}
}
if (!$fields) {
return null;
}
$property_lists = array();
foreach ($this->getDiffTabLabels($diffs) as $tab) {
list($label, $diff) = $tab;
$property_lists[] = array(
$label,
$this->buildDiffPropertyList($diff, $revision, $fields),
);
}
$tab_group = id(new PHUITabGroupView())
->setHideSingleTab(true);
foreach ($property_lists as $key => $property_list) {
list($tab_name, $list_view) = $property_list;
$tab = id(new PHUITabView())
->setKey($key)
->setName($tab_name)
->appendChild($list_view);
$tab_group->addTab($tab);
$tab_group->selectTab($key);
}
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Diff Detail'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setUser($viewer)
->addTabGroup($tab_group);
}
private function buildDiffPropertyList(
DifferentialDiff $diff,
DifferentialRevision $revision,
array $fields) {
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($diff);
foreach ($fields as $field) {
$label = $field->renderDiffPropertyViewLabel($diff);
$value = $field->renderDiffPropertyViewValue($diff);
if ($value !== null) {
$view->addProperty($label, $value);
}
}
return $view;
}
private function buildOperationsBox(DifferentialRevision $revision) {
$viewer = $this->getViewer();
// Save a query if we can't possibly have pending operations.
$repository = $revision->getRepository();
if (!$repository || !$repository->canPerformAutomation()) {
return null;
}
$operations = id(new DrydockRepositoryOperationQuery())
->setViewer($viewer)
->withObjectPHIDs(array($revision->getPHID()))
->withIsDismissed(false)
->withOperationTypes(
array(
DrydockLandRepositoryOperation::OPCONST,
))
->execute();
if (!$operations) {
return null;
}
$state_fail = DrydockRepositoryOperation::STATE_FAIL;
// We're going to show the oldest operation which hasn't failed, or the
// most recent failure if they're all failures.
$operations = msort($operations, 'getID');
foreach ($operations as $operation) {
if ($operation->getOperationState() != $state_fail) {
break;
}
}
// If we found a completed operation, don't render anything. We don't want
// to show an older error after the thing worked properly.
if ($operation->isDone()) {
return null;
}
$box_view = id(new PHUIObjectBoxView())
->setHeaderText(pht('Active Operations'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
return id(new DrydockRepositoryOperationStatusView())
->setUser($viewer)
->setBoxView($box_view)
->setOperation($operation);
}
private function buildUnitMessagesView(
$diff,
DifferentialRevision $revision) {
$viewer = $this->getViewer();
if (!$diff->getBuildable()) {
return null;
}
if (!$diff->getUnitMessages()) {
return null;
}
$interesting_messages = array();
foreach ($diff->getUnitMessages() as $message) {
switch ($message->getResult()) {
case ArcanistUnitTestResult::RESULT_PASS:
case ArcanistUnitTestResult::RESULT_SKIP:
break;
default:
$interesting_messages[] = $message;
break;
}
}
if (!$interesting_messages) {
return null;
}
$excuse = null;
if ($diff->hasDiffProperty('arc:unit-excuse')) {
$excuse = $diff->getProperty('arc:unit-excuse');
}
return id(new HarbormasterUnitSummaryView())
->setViewer($viewer)
->setExcuse($excuse)
->setBuildable($diff->getBuildable())
->setUnitMessages($diff->getUnitMessages())
->setLimit(5)
->setShowViewAll(true);
}
private function getOldDiffID(DifferentialRevision $revision, array $diffs) {
assert_instances_of($diffs, 'DifferentialDiff');
$request = $this->getRequest();
$diffs = mpull($diffs, null, 'getID');
$is_new = ($request->getURIData('filter') === 'new');
$old_id = $request->getInt('vs');
// This is ambiguous, so just 404 rather than trying to figure out what
// the user expects.
if ($is_new && $old_id) {
return new Aphront404Response();
}
if ($is_new) {
$viewer = $this->getViewer();
$xactions = id(new DifferentialTransactionQuery())
->setViewer($viewer)
->withObjectPHIDs(array($revision->getPHID()))
->withAuthorPHIDs(array($viewer->getPHID()))
->setOrder('newest')
->setLimit(1)
->execute();
if (!$xactions) {
$this->warnings[] = id(new PHUIInfoView())
->setTitle(pht('No Actions'))
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->appendChild(
pht(
'Showing all changes because you have never taken an '.
'action on this revision.'));
} else {
$xaction = head($xactions);
// Find the transactions which updated this revision. We want to
// figure out which diff was active when you last took an action.
$updates = id(new DifferentialTransactionQuery())
->setViewer($viewer)
->withObjectPHIDs(array($revision->getPHID()))
->withTransactionTypes(
array(
DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE,
))
->setOrder('oldest')
->execute();
// Sort the diffs into two buckets: those older than your last action
// and those newer than your last action.
$older = array();
$newer = array();
foreach ($updates as $update) {
// If you updated the revision with "arc diff", try to count that
// update as "before your last action".
if ($update->getDateCreated() <= $xaction->getDateCreated()) {
$older[] = $update->getNewValue();
} else {
$newer[] = $update->getNewValue();
}
}
if (!$newer) {
$this->warnings[] = id(new PHUIInfoView())
->setTitle(pht('No Recent Updates'))
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->appendChild(
pht(
'Showing all changes because the diff for this revision '.
'has not been updated since your last action.'));
} else {
$older = array_fuse($older);
// Find the most recent diff from before the last action.
$old = null;
foreach ($diffs as $diff) {
if (!isset($older[$diff->getPHID()])) {
break;
}
$old = $diff;
}
// It's possible we may not find such a diff: transactions may have
// been removed from the database, for example. If we miss, just
// fail into some reasonable state since 404'ing would be perplexing.
if ($old) {
$this->warnings[] = id(new PHUIInfoView())
->setTitle(pht('New Changes Shown'))
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->appendChild(
pht(
'Showing changes since the last action you took on this '.
'revision.'));
$old_id = $old->getID();
}
}
}
}
if (isset($diffs[$old_id])) {
return $old_id;
}
return null;
}
private function getNewDiffID(DifferentialRevision $revision, array $diffs) {
assert_instances_of($diffs, 'DifferentialDiff');
$request = $this->getRequest();
$diffs = mpull($diffs, null, 'getID');
$is_new = ($request->getURIData('filter') === 'new');
$new_id = $request->getInt('id');
if ($is_new && $new_id) {
return new Aphront404Response();
}
if (isset($diffs[$new_id])) {
return $new_id;
}
return (int)last_key($diffs);
}
}
diff --git a/src/applications/differential/editor/DifferentialDiffEditor.php b/src/applications/differential/editor/DifferentialDiffEditor.php
index 261f6f159..e78e08d80 100644
--- a/src/applications/differential/editor/DifferentialDiffEditor.php
+++ b/src/applications/differential/editor/DifferentialDiffEditor.php
@@ -1,238 +1,229 @@
<?php
final class DifferentialDiffEditor
extends PhabricatorApplicationTransactionEditor {
private $diffDataDict;
private $lookupRepository = true;
public function setLookupRepository($bool) {
$this->lookupRepository = $bool;
return $this;
}
public function getEditorApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
public function getEditorObjectsDescription() {
return pht('Differential Diffs');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = DifferentialDiffTransaction::TYPE_DIFF_CREATE;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialDiffTransaction::TYPE_DIFF_CREATE:
return null;
}
return parent::getCustomTransactionOldValue($object, $xaction);
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialDiffTransaction::TYPE_DIFF_CREATE:
$this->diffDataDict = $xaction->getNewValue();
return true;
}
return parent::getCustomTransactionNewValue($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialDiffTransaction::TYPE_DIFF_CREATE:
$dict = $this->diffDataDict;
$this->updateDiffFromDict($object, $dict);
return;
}
return parent::applyCustomInternalTransaction($object, $xaction);
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialDiffTransaction::TYPE_DIFF_CREATE:
return;
}
return parent::applyCustomExternalTransaction($object, $xaction);
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
// If we didn't get an explicit `repositoryPHID` (which means the client
// is old, or couldn't figure out which repository the working copy
// belongs to), apply heuristics to try to figure it out.
if ($this->lookupRepository && !$object->getRepositoryPHID()) {
$repository = id(new DifferentialRepositoryLookup())
->setDiff($object)
->setViewer($this->getActor())
->lookupRepository();
if ($repository) {
$object->setRepositoryPHID($repository->getPHID());
$object->setRepositoryUUID($repository->getUUID());
$object->save();
}
}
return $xactions;
}
/**
* We run Herald as part of transaction validation because Herald can
* block diff creation for Differential diffs. Its important to do this
* separately so no Herald logs are saved; these logs could expose
* information the Herald rules are intended to block.
*/
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = parent::validateTransaction($object, $type, $xactions);
foreach ($xactions as $xaction) {
switch ($type) {
case DifferentialDiffTransaction::TYPE_DIFF_CREATE:
$diff = clone $object;
$diff = $this->updateDiffFromDict($diff, $xaction->getNewValue());
$adapter = $this->buildHeraldAdapter($diff, $xactions);
$adapter->setContentSource($this->getContentSource());
$adapter->setIsNewObject($this->getIsNewObject());
$engine = new HeraldEngine();
$rules = $engine->loadRulesForAdapter($adapter);
$rules = mpull($rules, null, 'getID');
$effects = $engine->applyRules($rules, $adapter);
$action_block = DifferentialBlockHeraldAction::ACTIONCONST;
$blocking_effect = null;
foreach ($effects as $effect) {
if ($effect->getAction() == $action_block) {
$blocking_effect = $effect;
break;
}
}
if ($blocking_effect) {
$rule = $blocking_effect->getRule();
$message = $effect->getTarget();
if (!strlen($message)) {
$message = pht('(None.)');
}
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Rejected by Herald'),
pht(
"Creation of this diff was rejected by Herald rule %s.\n".
" Rule: %s\n".
"Reason: %s",
$rule->getMonogram(),
$rule->getName(),
$message));
}
break;
}
}
return $errors;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
protected function supportsSearch() {
return false;
}
/* -( Herald Integration )------------------------------------------------- */
/**
* See @{method:validateTransaction}. The only Herald action is to block
* the creation of Diffs. We thus have to be careful not to save any
* data and do this validation very early.
*/
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
$adapter = id(new HeraldDifferentialDiffAdapter())
->setDiff($object);
return $adapter;
}
- protected function didApplyHeraldRules(
- PhabricatorLiskDAO $object,
- HeraldAdapter $adapter,
- HeraldTranscript $transcript) {
-
- $xactions = array();
- return $xactions;
- }
-
private function updateDiffFromDict(DifferentialDiff $diff, $dict) {
$diff
->setSourcePath(idx($dict, 'sourcePath'))
->setSourceMachine(idx($dict, 'sourceMachine'))
->setBranch(idx($dict, 'branch'))
->setCreationMethod(idx($dict, 'creationMethod'))
->setAuthorPHID(idx($dict, 'authorPHID', $this->getActor()))
->setBookmark(idx($dict, 'bookmark'))
->setRepositoryPHID(idx($dict, 'repositoryPHID'))
->setRepositoryUUID(idx($dict, 'repositoryUUID'))
->setSourceControlSystem(idx($dict, 'sourceControlSystem'))
->setSourceControlPath(idx($dict, 'sourceControlPath'))
->setSourceControlBaseRevision(idx($dict, 'sourceControlBaseRevision'))
->setLintStatus(idx($dict, 'lintStatus'))
->setUnitStatus(idx($dict, 'unitStatus'));
return $diff;
}
}
diff --git a/src/applications/differential/engine/DifferentialChangesetEngine.php b/src/applications/differential/engine/DifferentialChangesetEngine.php
index d72db025a..23382e6a8 100644
--- a/src/applications/differential/engine/DifferentialChangesetEngine.php
+++ b/src/applications/differential/engine/DifferentialChangesetEngine.php
@@ -1,262 +1,268 @@
<?php
final class DifferentialChangesetEngine extends Phobject {
public function rebuildChangesets(array $changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
foreach ($changesets as $changeset) {
$this->detectGeneratedCode($changeset);
$this->computeHashes($changeset);
}
$this->detectCopiedCode($changesets);
}
/* -( Generated Code )----------------------------------------------------- */
private function detectGeneratedCode(DifferentialChangeset $changeset) {
$is_generated_trusted = $this->isTrustedGeneratedCode($changeset);
if ($is_generated_trusted) {
$changeset->setTrustedChangesetAttribute(
DifferentialChangeset::ATTRIBUTE_GENERATED,
$is_generated_trusted);
}
$is_generated_untrusted = $this->isUntrustedGeneratedCode($changeset);
if ($is_generated_untrusted) {
$changeset->setUntrustedChangesetAttribute(
DifferentialChangeset::ATTRIBUTE_GENERATED,
$is_generated_untrusted);
}
}
private function isTrustedGeneratedCode(DifferentialChangeset $changeset) {
$filename = $changeset->getFilename();
$paths = PhabricatorEnv::getEnvConfig('differential.generated-paths');
foreach ($paths as $regexp) {
if (preg_match($regexp, $filename)) {
return true;
}
}
return false;
}
private function isUntrustedGeneratedCode(DifferentialChangeset $changeset) {
if ($changeset->getHunks()) {
$new_data = $changeset->makeNewFile();
if (strpos($new_data, '@'.'generated') !== false) {
return true;
}
+
+ // See PHI1112. This is the official pattern for marking Go code as
+ // generated.
+ if (preg_match('(^// Code generated .* DO NOT EDIT\.$)m', $new_data)) {
+ return true;
+ }
}
return false;
}
/* -( Content Hashes )----------------------------------------------------- */
private function computeHashes(DifferentialChangeset $changeset) {
$effect_key = DifferentialChangeset::METADATA_EFFECT_HASH;
$effect_hash = $this->newEffectHash($changeset);
if ($effect_hash !== null) {
$changeset->setChangesetMetadata($effect_key, $effect_hash);
}
}
private function newEffectHash(DifferentialChangeset $changeset) {
if ($changeset->getHunks()) {
$new_data = $changeset->makeNewFile();
return PhabricatorHash::digestForIndex($new_data);
}
return null;
}
/* -( Copied Code )-------------------------------------------------------- */
private function detectCopiedCode(array $changesets) {
// See PHI944. If the total number of changed lines is excessively large,
// don't bother with copied code detection. This can take a lot of time and
// memory and it's not generally of any use for very large changes.
$max_size = 65535;
$total_size = 0;
foreach ($changesets as $changeset) {
$total_size += ($changeset->getAddLines() + $changeset->getDelLines());
}
if ($total_size > $max_size) {
return;
}
$min_width = 30;
$min_lines = 3;
$map = array();
$files = array();
$types = array();
foreach ($changesets as $changeset) {
$file = $changeset->getFilename();
foreach ($changeset->getHunks() as $hunk) {
$lines = $hunk->getStructuredOldFile();
foreach ($lines as $line => $info) {
$type = $info['type'];
if ($type == '\\') {
continue;
}
$types[$file][$line] = $type;
$text = $info['text'];
$text = trim($text);
$files[$file][$line] = $text;
if (strlen($text) >= $min_width) {
$map[$text][] = array($file, $line);
}
}
}
}
foreach ($changesets as $changeset) {
$copies = array();
foreach ($changeset->getHunks() as $hunk) {
$added = $hunk->getStructuredNewFile();
$atype = array();
foreach ($added as $line => $info) {
$atype[$line] = $info['type'];
$added[$line] = trim($info['text']);
}
$skip_lines = 0;
foreach ($added as $line => $code) {
if ($skip_lines) {
// We're skipping lines that we already processed because we
// extended a block above them downward to include them.
$skip_lines--;
continue;
}
if ($atype[$line] !== '+') {
// This line hasn't been changed in the new file, so don't try
// to figure out where it came from.
continue;
}
if (empty($map[$code])) {
// This line was too short to trigger copy/move detection.
continue;
}
if (count($map[$code]) > 16) {
// If there are a large number of identical lines in this diff,
// don't try to figure out where this block came from: the analysis
// is O(N^2), since we need to compare every line against every
// other line. Even if we arrive at a result, it is unlikely to be
// meaningful. See T5041.
continue;
}
$best_length = 0;
// Explore all candidates.
foreach ($map[$code] as $val) {
list($file, $orig_line) = $val;
$length = 1;
// Search backward and forward to find all of the adjacent lines
// which match.
foreach (array(-1, 1) as $direction) {
$offset = $direction;
while (true) {
if (isset($copies[$line + $offset])) {
// If we run into a block above us which we've already
// attributed to a move or copy from elsewhere, stop
// looking.
break;
}
if (!isset($added[$line + $offset])) {
// If we've run off the beginning or end of the new file,
// stop looking.
break;
}
if (!isset($files[$file][$orig_line + $offset])) {
// If we've run off the beginning or end of the original
// file, we also stop looking.
break;
}
$old = $files[$file][$orig_line + $offset];
$new = $added[$line + $offset];
if ($old !== $new) {
// If the old line doesn't match the new line, stop
// looking.
break;
}
$length++;
$offset += $direction;
}
}
if ($length < $best_length) {
// If we already know of a better source (more matching lines)
// for this move/copy, stick with that one. We prefer long
// copies/moves which match a lot of context over short ones.
continue;
}
if ($length == $best_length) {
if (idx($types[$file], $orig_line) != '-') {
// If we already know of an equally good source (same number
// of matching lines) and this isn't a move, stick with the
// other one. We prefer moves over copies.
continue;
}
}
$best_length = $length;
// ($offset - 1) contains number of forward matching lines.
$best_offset = $offset - 1;
$best_file = $file;
$best_line = $orig_line;
}
$file = ($best_file == $changeset->getFilename() ? '' : $best_file);
for ($i = $best_length; $i--; ) {
$type = idx($types[$best_file], $best_line + $best_offset - $i);
$copies[$line + $best_offset - $i] = ($best_length < $min_lines
? array() // Ignore short blocks.
: array($file, $best_line + $best_offset - $i, $type));
}
$skip_lines = $best_offset;
}
}
$copies = array_filter($copies);
if ($copies) {
$metadata = $changeset->getMetadata();
$metadata['copy:lines'] = $copies;
$changeset->setMetadata($metadata);
}
}
}
}
diff --git a/src/applications/differential/engine/DifferentialDiffExtractionEngine.php b/src/applications/differential/engine/DifferentialDiffExtractionEngine.php
index 861d2ad22..7b94b1958 100644
--- a/src/applications/differential/engine/DifferentialDiffExtractionEngine.php
+++ b/src/applications/differential/engine/DifferentialDiffExtractionEngine.php
@@ -1,325 +1,420 @@
<?php
final class DifferentialDiffExtractionEngine extends Phobject {
private $viewer;
private $authorPHID;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setAuthorPHID($author_phid) {
$this->authorPHID = $author_phid;
return $this;
}
public function getAuthorPHID() {
return $this->authorPHID;
}
public function newDiffFromCommit(PhabricatorRepositoryCommit $commit) {
$viewer = $this->getViewer();
// If we already have an unattached diff for this commit, just reuse it.
// This stops us from repeatedly generating diffs if something goes wrong
// later in the process. See T10968 for context.
$existing_diffs = id(new DifferentialDiffQuery())
->setViewer($viewer)
->withCommitPHIDs(array($commit->getPHID()))
->withHasRevision(false)
->needChangesets(true)
->execute();
if ($existing_diffs) {
return head($existing_diffs);
}
$repository = $commit->getRepository();
$identifier = $commit->getCommitIdentifier();
$monogram = $commit->getMonogram();
$drequest = DiffusionRequest::newFromDictionary(
array(
'user' => $viewer,
'repository' => $repository,
));
$diff_info = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
$drequest,
'diffusion.rawdiffquery',
array(
'commit' => $identifier,
));
$file_phid = $diff_info['filePHID'];
$diff_file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($file_phid))
->executeOne();
if (!$diff_file) {
throw new Exception(
pht(
'Failed to load file ("%s") returned by "%s".',
$file_phid,
'diffusion.rawdiffquery'));
}
$raw_diff = $diff_file->loadFileData();
// TODO: Support adds, deletes and moves under SVN.
if (strlen($raw_diff)) {
$changes = id(new ArcanistDiffParser())->parseDiff($raw_diff);
} else {
// This is an empty diff, maybe made with `git commit --allow-empty`.
// NOTE: These diffs have the same tree hash as their ancestors, so
// they may attach to revisions in an unexpected way. Just let this
// happen for now, although it might make sense to special case it
// eventually.
$changes = array();
}
$diff = DifferentialDiff::newFromRawChanges($viewer, $changes)
->setRepositoryPHID($repository->getPHID())
->setCommitPHID($commit->getPHID())
->setCreationMethod('commit')
->setSourceControlSystem($repository->getVersionControlSystem())
->setLintStatus(DifferentialLintStatus::LINT_AUTO_SKIP)
->setUnitStatus(DifferentialUnitStatus::UNIT_AUTO_SKIP)
->setDateCreated($commit->getEpoch())
->setDescription($monogram);
$author_phid = $this->getAuthorPHID();
if ($author_phid !== null) {
$diff->setAuthorPHID($author_phid);
}
$parents = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
$drequest,
'diffusion.commitparentsquery',
array(
'commit' => $identifier,
));
if ($parents) {
$diff->setSourceControlBaseRevision(head($parents));
}
// TODO: Attach binary files.
return $diff->save();
}
public function isDiffChangedBeforeCommit(
PhabricatorRepositoryCommit $commit,
DifferentialDiff $old,
DifferentialDiff $new) {
$viewer = $this->getViewer();
$repository = $commit->getRepository();
$identifier = $commit->getCommitIdentifier();
$vs_changesets = array();
foreach ($old->getChangesets() as $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $old);
$path = ltrim($path, '/');
$vs_changesets[$path] = $changeset;
}
$changesets = array();
foreach ($new->getChangesets() as $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $new);
$path = ltrim($path, '/');
$changesets[$path] = $changeset;
}
if (array_fill_keys(array_keys($changesets), true) !=
array_fill_keys(array_keys($vs_changesets), true)) {
return true;
}
$file_phids = array();
foreach ($vs_changesets as $changeset) {
$metadata = $changeset->getMetadata();
$file_phid = idx($metadata, 'new:binary-phid');
if ($file_phid) {
$file_phids[$file_phid] = $file_phid;
}
}
$files = array();
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
}
foreach ($changesets as $path => $changeset) {
$vs_changeset = $vs_changesets[$path];
$file_phid = idx($vs_changeset->getMetadata(), 'new:binary-phid');
if ($file_phid) {
if (!isset($files[$file_phid])) {
return true;
}
$drequest = DiffusionRequest::newFromDictionary(
array(
'user' => $viewer,
'repository' => $repository,
));
try {
$response = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
$drequest,
'diffusion.filecontentquery',
array(
'commit' => $identifier,
'path' => $path,
));
} catch (Exception $ex) {
// TODO: See PHI1044. This call may fail if the diff deleted the
// file. If the call fails, just detect a change for now. This should
// generally be made cleaner in the future.
return true;
}
$new_file_phid = $response['filePHID'];
if (!$new_file_phid) {
return true;
}
$new_file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($new_file_phid))
->executeOne();
if (!$new_file) {
return true;
}
if ($files[$file_phid]->loadFileData() != $new_file->loadFileData()) {
return true;
}
} else {
$context = implode("\n", $changeset->makeChangesWithContext());
$vs_context = implode("\n", $vs_changeset->makeChangesWithContext());
// We couldn't just compare $context and $vs_context because following
// diffs will be considered different:
//
// -(empty line)
// -echo 'test';
// (empty line)
//
// (empty line)
// -echo "test";
// -(empty line)
$hunk = id(new DifferentialHunk())->setChanges($context);
$vs_hunk = id(new DifferentialHunk())->setChanges($vs_context);
if ($hunk->makeOldFile() != $vs_hunk->makeOldFile() ||
$hunk->makeNewFile() != $vs_hunk->makeNewFile()) {
return true;
}
}
}
return false;
}
public function updateRevisionWithCommit(
DifferentialRevision $revision,
PhabricatorRepositoryCommit $commit,
array $more_xactions,
PhabricatorContentSource $content_source) {
$viewer = $this->getViewer();
$result_data = array();
$new_diff = $this->newDiffFromCommit($commit);
$old_diff = $revision->getActiveDiff();
$changed_uri = null;
if ($old_diff) {
$old_diff = id(new DifferentialDiffQuery())
->setViewer($viewer)
->withIDs(array($old_diff->getID()))
->needChangesets(true)
->executeOne();
if ($old_diff) {
$has_changed = $this->isDiffChangedBeforeCommit(
$commit,
$old_diff,
$new_diff);
if ($has_changed) {
$result_data['vsDiff'] = $old_diff->getID();
$revision_monogram = $revision->getMonogram();
$old_id = $old_diff->getID();
$new_id = $new_diff->getID();
$changed_uri = "/{$revision_monogram}?vs={$old_id}&id={$new_id}#toc";
$changed_uri = PhabricatorEnv::getProductionURI($changed_uri);
}
}
}
$xactions = array();
// If the revision isn't closed or "Accepted", write a warning into the
// transaction log. This makes it more clear when users bend the rules.
if (!$revision->isClosed() && !$revision->isAccepted()) {
$wrong_type = DifferentialRevisionWrongStateTransaction::TRANSACTIONTYPE;
$xactions[] = id(new DifferentialTransaction())
->setTransactionType($wrong_type)
->setNewValue($revision->getModernRevisionStatus());
}
+ $concerning_builds = $this->loadConcerningBuilds($revision);
+ if ($concerning_builds) {
+ $build_list = array();
+ foreach ($concerning_builds as $build) {
+ $build_list[] = array(
+ 'phid' => $build->getPHID(),
+ 'status' => $build->getBuildStatus(),
+ );
+ }
+
+ $wrong_builds =
+ DifferentialRevisionWrongBuildsTransaction::TRANSACTIONTYPE;
+
+ $xactions[] = id(new DifferentialTransaction())
+ ->setTransactionType($wrong_builds)
+ ->setNewValue($build_list);
+ }
+
$type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE;
$xactions[] = id(new DifferentialTransaction())
->setTransactionType($type_update)
->setIgnoreOnNoEffect(true)
->setNewValue($new_diff->getPHID())
->setMetadataValue('isCommitUpdate', true)
->setMetadataValue('commitPHIDs', array($commit->getPHID()));
foreach ($more_xactions as $more_xaction) {
$xactions[] = $more_xaction;
}
$editor = id(new DifferentialTransactionEditor())
->setActor($viewer)
->setContinueOnMissingFields(true)
->setContentSource($content_source)
->setChangedPriorToCommitURI($changed_uri)
->setIsCloseByCommit(true);
$author_phid = $this->getAuthorPHID();
if ($author_phid !== null) {
$editor->setActingAsPHID($author_phid);
}
try {
$editor->applyTransactions($revision, $xactions);
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
// NOTE: We've marked transactions other than the CLOSE transaction
// as ignored when they don't have an effect, so this means that we
// lost a race to close the revision. That's perfectly fine, we can
// just continue normally.
}
return $result_data;
}
+ private function loadConcerningBuilds(DifferentialRevision $revision) {
+ $viewer = $this->getViewer();
+ $diff = $revision->getActiveDiff();
+
+ $buildables = id(new HarbormasterBuildableQuery())
+ ->setViewer($viewer)
+ ->withBuildablePHIDs(array($diff->getPHID()))
+ ->needBuilds(true)
+ ->withManualBuildables(false)
+ ->execute();
+ if (!$buildables) {
+ return array();
+ }
+
+
+ $land_key = HarbormasterBuildPlanBehavior::BEHAVIOR_LANDWARNING;
+ $behavior = HarbormasterBuildPlanBehavior::getBehavior($land_key);
+
+ $key_never = HarbormasterBuildPlanBehavior::LANDWARNING_NEVER;
+ $key_building = HarbormasterBuildPlanBehavior::LANDWARNING_IF_BUILDING;
+ $key_complete = HarbormasterBuildPlanBehavior::LANDWARNING_IF_COMPLETE;
+
+ $concerning_builds = array();
+ foreach ($buildables as $buildable) {
+ $builds = $buildable->getBuilds();
+ foreach ($builds as $build) {
+ $plan = $build->getBuildPlan();
+ $option = $behavior->getPlanOption($plan);
+ $behavior_value = $option->getKey();
+
+ $if_never = ($behavior_value === $key_never);
+ if ($if_never) {
+ continue;
+ }
+
+ $if_building = ($behavior_value === $key_building);
+ if ($if_building && $build->isComplete()) {
+ continue;
+ }
+
+ $if_complete = ($behavior_value === $key_complete);
+ if ($if_complete) {
+ if (!$build->isComplete()) {
+ continue;
+ }
+
+ // TODO: If you "arc land" and a build with "Warn: If Complete"
+ // is still running, you may not see a warning, and push the revision
+ // in good faith. The build may then complete before we get here, so
+ // we now see a completed, failed build.
+
+ // For now, just err on the side of caution and assume these builds
+ // were in a good state when we prompted the user, even if they're in
+ // a bad state now.
+
+ // We could refine this with a rule like "if the build finished
+ // within a couple of minutes before the push happened, assume it was
+ // in good faith", but we don't currently have an especially
+ // convenient way to check when the build finished or when the commit
+ // was pushed or discovered, and this would create some issues in
+ // cases where the repository is observed and the fetch pipeline
+ // stalls for a while.
+
+ continue;
+ }
+
+ if ($build->isPassed()) {
+ continue;
+ }
+
+ $concerning_builds[] = $build;
+ }
+ }
+
+ return $concerning_builds;
+ }
+
}
diff --git a/src/applications/differential/harbormaster/DifferentialBuildableEngine.php b/src/applications/differential/harbormaster/DifferentialBuildableEngine.php
index 8554f7be2..8565c2dca 100644
--- a/src/applications/differential/harbormaster/DifferentialBuildableEngine.php
+++ b/src/applications/differential/harbormaster/DifferentialBuildableEngine.php
@@ -1,81 +1,85 @@
<?php
final class DifferentialBuildableEngine
extends HarbormasterBuildableEngine {
protected function getPublishableObject() {
$object = $this->getObject();
if ($object instanceof DifferentialDiff) {
- return $object->getRevision();
+ if ($object->getRevisionID()) {
+ return $object->getRevision();
+ } else {
+ return null;
+ }
}
return $object;
}
public function publishBuildable(
HarbormasterBuildable $old,
HarbormasterBuildable $new) {
// If we're publishing to a diff that is not actually attached to a
// revision, we have nothing to publish to, so just bail out.
$revision = $this->getPublishableObject();
if (!$revision) {
return;
}
// Don't publish manual buildables.
if ($new->getIsManualBuildable()) {
return;
}
// Don't publish anything if the buildable is still building. Differential
// treats more buildables as "building" than Harbormaster does, but the
// Differential definition is a superset of the Harbormaster definition.
if ($new->isBuilding()) {
return;
}
$viewer = $this->getViewer();
$old_status = $revision->getBuildableStatus($new->getPHID());
$new_status = $revision->newBuildableStatus($viewer, $new->getPHID());
if ($old_status === $new_status) {
return;
}
$buildable_type = DifferentialRevisionBuildableTransaction::TRANSACTIONTYPE;
$xaction = $this->newTransaction()
->setMetadataValue('harbormaster:buildablePHID', $new->getPHID())
->setTransactionType($buildable_type)
->setNewValue($new_status);
$this->applyTransactions(array($xaction));
}
public function getAuthorIdentity() {
$object = $this->getObject();
if ($object instanceof DifferentialRevision) {
$object = $object->loadActiveDiff();
}
$authorship = $object->getDiffAuthorshipDict();
if (!isset($authorship['authorName'])) {
return null;
}
$name = $authorship['authorName'];
$address = idx($authorship, 'authorEmail');
$full = id(new PhutilEmailAddress())
->setDisplayName($name)
->setAddress($address);
return id(new PhabricatorRepositoryIdentity())
->setIdentityName((string)$full)
->makeEphemeral();
}
}
diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php
index e214aa16a..8ed6d80ee 100644
--- a/src/applications/differential/parser/DifferentialChangesetParser.php
+++ b/src/applications/differential/parser/DifferentialChangesetParser.php
@@ -1,1490 +1,1603 @@
<?php
final class DifferentialChangesetParser extends Phobject {
const HIGHLIGHT_BYTE_LIMIT = 262144;
protected $visible = array();
protected $new = array();
protected $old = array();
protected $intra = array();
+ protected $depthOnlyLines = array();
protected $newRender = null;
protected $oldRender = null;
protected $filename = null;
protected $hunkStartLines = array();
protected $comments = array();
protected $specialAttributes = array();
protected $changeset;
- protected $whitespaceMode = null;
protected $renderCacheKey = null;
private $handles = array();
private $user;
private $leftSideChangesetID;
private $leftSideAttachesToNewFile;
private $rightSideChangesetID;
private $rightSideAttachesToNewFile;
private $originalLeft;
private $originalRight;
private $renderingReference;
private $isSubparser;
private $isTopLevel;
private $coverage;
private $markupEngine;
private $highlightErrors;
private $disableCache;
private $renderer;
private $characterEncoding;
private $highlightAs;
private $highlightingDisabled;
private $showEditAndReplyLinks = true;
private $canMarkDone;
private $objectOwnerPHID;
private $offsetMode;
private $rangeStart;
private $rangeEnd;
private $mask;
private $linesOfContext = 8;
private $highlightEngine;
public function setRange($start, $end) {
$this->rangeStart = $start;
$this->rangeEnd = $end;
return $this;
}
public function setMask(array $mask) {
$this->mask = $mask;
return $this;
}
public function renderChangeset() {
return $this->render($this->rangeStart, $this->rangeEnd, $this->mask);
}
public function setShowEditAndReplyLinks($bool) {
$this->showEditAndReplyLinks = $bool;
return $this;
}
public function getShowEditAndReplyLinks() {
return $this->showEditAndReplyLinks;
}
public function setHighlightAs($highlight_as) {
$this->highlightAs = $highlight_as;
return $this;
}
public function getHighlightAs() {
return $this->highlightAs;
}
public function setCharacterEncoding($character_encoding) {
$this->characterEncoding = $character_encoding;
return $this;
}
public function getCharacterEncoding() {
return $this->characterEncoding;
}
public function setRenderer(DifferentialChangesetRenderer $renderer) {
$this->renderer = $renderer;
return $this;
}
public function getRenderer() {
if (!$this->renderer) {
return new DifferentialChangesetTwoUpRenderer();
}
return $this->renderer;
}
public function setDisableCache($disable_cache) {
$this->disableCache = $disable_cache;
return $this;
}
public function getDisableCache() {
return $this->disableCache;
}
public function setCanMarkDone($can_mark_done) {
$this->canMarkDone = $can_mark_done;
return $this;
}
public function getCanMarkDone() {
return $this->canMarkDone;
}
public function setObjectOwnerPHID($phid) {
$this->objectOwnerPHID = $phid;
return $this;
}
public function getObjectOwnerPHID() {
return $this->objectOwnerPHID;
}
public function setOffsetMode($offset_mode) {
$this->offsetMode = $offset_mode;
return $this;
}
public function getOffsetMode() {
return $this->offsetMode;
}
public static function getDefaultRendererForViewer(PhabricatorUser $viewer) {
$is_unified = $viewer->compareUserSetting(
PhabricatorUnifiedDiffsSetting::SETTINGKEY,
PhabricatorUnifiedDiffsSetting::VALUE_ALWAYS_UNIFIED);
if ($is_unified) {
return '1up';
}
return null;
}
public function readParametersFromRequest(AphrontRequest $request) {
- $this->setWhitespaceMode($request->getStr('whitespace'));
$this->setCharacterEncoding($request->getStr('encoding'));
$this->setHighlightAs($request->getStr('highlight'));
$renderer = null;
// If the viewer prefers unified diffs, always set the renderer to unified.
// Otherwise, we leave it unspecified and the client will choose a
// renderer based on the screen size.
if ($request->getStr('renderer')) {
$renderer = $request->getStr('renderer');
} else {
$renderer = self::getDefaultRendererForViewer($request->getViewer());
}
switch ($renderer) {
case '1up':
$this->setRenderer(new DifferentialChangesetOneUpRenderer());
break;
default:
$this->setRenderer(new DifferentialChangesetTwoUpRenderer());
break;
}
return $this;
}
- const CACHE_VERSION = 11;
+ const CACHE_VERSION = 14;
const CACHE_MAX_SIZE = 8e6;
const ATTR_GENERATED = 'attr:generated';
const ATTR_DELETED = 'attr:deleted';
const ATTR_UNCHANGED = 'attr:unchanged';
- const ATTR_WHITELINES = 'attr:white';
const ATTR_MOVEAWAY = 'attr:moveaway';
- const WHITESPACE_SHOW_ALL = 'show-all';
- const WHITESPACE_IGNORE_TRAILING = 'ignore-trailing';
- const WHITESPACE_IGNORE_MOST = 'ignore-most';
- const WHITESPACE_IGNORE_ALL = 'ignore-all';
-
public function setOldLines(array $lines) {
$this->old = $lines;
return $this;
}
public function setNewLines(array $lines) {
$this->new = $lines;
return $this;
}
public function setSpecialAttributes(array $attributes) {
$this->specialAttributes = $attributes;
return $this;
}
public function setIntraLineDiffs(array $diffs) {
$this->intra = $diffs;
return $this;
}
+ public function setDepthOnlyLines(array $lines) {
+ $this->depthOnlyLines = $lines;
+ return $this;
+ }
+
+ public function getDepthOnlyLines() {
+ return $this->depthOnlyLines;
+ }
+
public function setVisibileLinesMask(array $mask) {
$this->visible = $mask;
return $this;
}
public function setLinesOfContext($lines_of_context) {
$this->linesOfContext = $lines_of_context;
return $this;
}
public function getLinesOfContext() {
return $this->linesOfContext;
}
/**
* Configure which Changeset comments added to the right side of the visible
* diff will be attached to. The ID must be the ID of a real Differential
* Changeset.
*
* The complexity here is that we may show an arbitrary side of an arbitrary
* changeset as either the left or right part of a diff. This method allows
* the left and right halves of the displayed diff to be correctly mapped to
* storage changesets.
*
* @param id The Differential Changeset ID that comments added to the right
* side of the visible diff should be attached to.
* @param bool If true, attach new comments to the right side of the storage
* changeset. Note that this may be false, if the left side of
* some storage changeset is being shown as the right side of
* a display diff.
* @return this
*/
public function setRightSideCommentMapping($id, $is_new) {
$this->rightSideChangesetID = $id;
$this->rightSideAttachesToNewFile = $is_new;
return $this;
}
/**
* See setRightSideCommentMapping(), but this sets information for the left
* side of the display diff.
*/
public function setLeftSideCommentMapping($id, $is_new) {
$this->leftSideChangesetID = $id;
$this->leftSideAttachesToNewFile = $is_new;
return $this;
}
public function setOriginals(
DifferentialChangeset $left,
DifferentialChangeset $right) {
$this->originalLeft = $left;
$this->originalRight = $right;
return $this;
}
public function diffOriginals() {
$engine = new PhabricatorDifferenceEngine();
$changeset = $engine->generateChangesetFromFileContent(
implode('', mpull($this->originalLeft->getHunks(), 'getChanges')),
implode('', mpull($this->originalRight->getHunks(), 'getChanges')));
$parser = new DifferentialHunkParser();
return $parser->parseHunksForHighlightMasks(
$changeset->getHunks(),
$this->originalLeft->getHunks(),
$this->originalRight->getHunks());
}
/**
* Set a key for identifying this changeset in the render cache. If set, the
* parser will attempt to use the changeset render cache, which can improve
* performance for frequently-viewed changesets.
*
* By default, there is no render cache key and parsers do not use the cache.
* This is appropriate for rarely-viewed changesets.
*
* NOTE: Currently, this key must be a valid Differential Changeset ID.
*
* @param string Key for identifying this changeset in the render cache.
* @return this
*/
public function setRenderCacheKey($key) {
$this->renderCacheKey = $key;
return $this;
}
private function getRenderCacheKey() {
return $this->renderCacheKey;
}
public function setChangeset(DifferentialChangeset $changeset) {
$this->changeset = $changeset;
$this->setFilename($changeset->getFilename());
return $this;
}
- public function setWhitespaceMode($whitespace_mode) {
- $this->whitespaceMode = $whitespace_mode;
- return $this;
- }
-
public function setRenderingReference($ref) {
$this->renderingReference = $ref;
return $this;
}
private function getRenderingReference() {
return $this->renderingReference;
}
public function getChangeset() {
return $this->changeset;
}
public function setFilename($filename) {
$this->filename = $filename;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setMarkupEngine(PhabricatorMarkupEngine $engine) {
$this->markupEngine = $engine;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function getUser() {
return $this->user;
}
public function setCoverage($coverage) {
$this->coverage = $coverage;
return $this;
}
private function getCoverage() {
return $this->coverage;
}
public function parseInlineComment(
PhabricatorInlineCommentInterface $comment) {
// Parse only comments which are actually visible.
if ($this->isCommentVisibleOnRenderedDiff($comment)) {
$this->comments[] = $comment;
}
return $this;
}
private function loadCache() {
$render_cache_key = $this->getRenderCacheKey();
if (!$render_cache_key) {
return false;
}
$data = null;
$changeset = new DifferentialChangeset();
$conn_r = $changeset->establishConnection('r');
$data = queryfx_one(
$conn_r,
'SELECT * FROM %T WHERE id = %d',
$changeset->getTableName().'_parse_cache',
$render_cache_key);
if (!$data) {
return false;
}
if ($data['cache'][0] == '{') {
// This is likely an old-style JSON cache which we will not be able to
// deserialize.
return false;
}
$data = unserialize($data['cache']);
if (!is_array($data) || !$data) {
return false;
}
foreach (self::getCacheableProperties() as $cache_key) {
if (!array_key_exists($cache_key, $data)) {
// If we're missing a cache key, assume we're looking at an old cache
// and ignore it.
return false;
}
}
if ($data['cacheVersion'] !== self::CACHE_VERSION) {
return false;
}
// Someone displays contents of a partially cached shielded file.
if (!isset($data['newRender']) && (!$this->isTopLevel || $this->comments)) {
return false;
}
unset($data['cacheVersion'], $data['cacheHost']);
$cache_prop = array_select_keys($data, self::getCacheableProperties());
foreach ($cache_prop as $cache_key => $v) {
$this->$cache_key = $v;
}
return true;
}
protected static function getCacheableProperties() {
return array(
'visible',
'new',
'old',
'intra',
+ 'depthOnlyLines',
'newRender',
'oldRender',
'specialAttributes',
'hunkStartLines',
'cacheVersion',
'cacheHost',
'highlightingDisabled',
);
}
public function saveCache() {
if (PhabricatorEnv::isReadOnly()) {
return false;
}
if ($this->highlightErrors) {
return false;
}
$render_cache_key = $this->getRenderCacheKey();
if (!$render_cache_key) {
return false;
}
$cache = array();
foreach (self::getCacheableProperties() as $cache_key) {
switch ($cache_key) {
case 'cacheVersion':
$cache[$cache_key] = self::CACHE_VERSION;
break;
case 'cacheHost':
$cache[$cache_key] = php_uname('n');
break;
default:
$cache[$cache_key] = $this->$cache_key;
break;
}
}
$cache = serialize($cache);
// We don't want to waste too much space by a single changeset.
if (strlen($cache) > self::CACHE_MAX_SIZE) {
return;
}
$changeset = new DifferentialChangeset();
$conn_w = $changeset->establishConnection('w');
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
try {
queryfx(
$conn_w,
'INSERT INTO %T (id, cache, dateCreated) VALUES (%d, %B, %d)
ON DUPLICATE KEY UPDATE cache = VALUES(cache)',
DifferentialChangeset::TABLE_CACHE,
$render_cache_key,
$cache,
time());
} catch (AphrontQueryException $ex) {
// Ignore these exceptions. A common cause is that the cache is
// larger than 'max_allowed_packet', in which case we're better off
// not writing it.
// TODO: It would be nice to tailor this more narrowly.
}
unset($unguarded);
}
private function markGenerated($new_corpus_block = '') {
$generated_guess = (strpos($new_corpus_block, '@'.'generated') !== false);
if (!$generated_guess) {
$generated_path_regexps = PhabricatorEnv::getEnvConfig(
'differential.generated-paths');
foreach ($generated_path_regexps as $regexp) {
if (preg_match($regexp, $this->changeset->getFilename())) {
$generated_guess = true;
break;
}
}
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_DIFFERENTIAL_WILLMARKGENERATED,
array(
'corpus' => $new_corpus_block,
'is_generated' => $generated_guess,
)
);
PhutilEventEngine::dispatchEvent($event);
$generated = $event->getValue('is_generated');
$attribute = $this->changeset->isGeneratedChangeset();
if ($attribute) {
$generated = true;
}
$this->specialAttributes[self::ATTR_GENERATED] = $generated;
}
public function isGenerated() {
return idx($this->specialAttributes, self::ATTR_GENERATED, false);
}
public function isDeleted() {
return idx($this->specialAttributes, self::ATTR_DELETED, false);
}
public function isUnchanged() {
return idx($this->specialAttributes, self::ATTR_UNCHANGED, false);
}
- public function isWhitespaceOnly() {
- return idx($this->specialAttributes, self::ATTR_WHITELINES, false);
- }
-
public function isMoveAway() {
return idx($this->specialAttributes, self::ATTR_MOVEAWAY, false);
}
private function applyIntraline(&$render, $intra, $corpus) {
foreach ($render as $key => $text) {
+ $result = $text;
+
if (isset($intra[$key])) {
- $render[$key] = ArcanistDiffUtils::applyIntralineDiff(
- $text,
+ $result = ArcanistDiffUtils::applyIntralineDiff(
+ $result,
$intra[$key]);
}
+
+ $result = $this->adjustRenderedLineForDisplay($result);
+
+ $render[$key] = $result;
}
}
private function getHighlightFuture($corpus) {
$language = $this->highlightAs;
if (!$language) {
$language = $this->highlightEngine->getLanguageFromFilename(
$this->filename);
if (($language != 'txt') &&
(strlen($corpus) > self::HIGHLIGHT_BYTE_LIMIT)) {
$this->highlightingDisabled = true;
$language = 'txt';
}
}
return $this->highlightEngine->getHighlightFuture(
$language,
$corpus);
}
protected function processHighlightedSource($data, $result) {
$result_lines = phutil_split_lines($result);
foreach ($data as $key => $info) {
if (!$info) {
unset($result_lines[$key]);
}
}
return $result_lines;
}
private function tryCacheStuff() {
- $whitespace_mode = $this->whitespaceMode;
- switch ($whitespace_mode) {
- case self::WHITESPACE_SHOW_ALL:
- case self::WHITESPACE_IGNORE_TRAILING:
- case self::WHITESPACE_IGNORE_ALL:
- break;
- default:
- $whitespace_mode = self::WHITESPACE_IGNORE_MOST;
- break;
- }
+ $skip_cache = false;
- $skip_cache = ($whitespace_mode != self::WHITESPACE_IGNORE_MOST);
if ($this->disableCache) {
$skip_cache = true;
}
if ($this->characterEncoding) {
$skip_cache = true;
}
if ($this->highlightAs) {
$skip_cache = true;
}
- $this->whitespaceMode = $whitespace_mode;
-
$changeset = $this->changeset;
if ($changeset->getFileType() != DifferentialChangeType::FILE_TEXT &&
$changeset->getFileType() != DifferentialChangeType::FILE_SYMLINK) {
$this->markGenerated();
} else {
if ($skip_cache || !$this->loadCache()) {
$this->process();
if (!$skip_cache) {
$this->saveCache();
}
}
}
}
private function process() {
- $whitespace_mode = $this->whitespaceMode;
$changeset = $this->changeset;
- $ignore_all = (($whitespace_mode == self::WHITESPACE_IGNORE_MOST) ||
- ($whitespace_mode == self::WHITESPACE_IGNORE_ALL));
-
- $force_ignore = ($whitespace_mode == self::WHITESPACE_IGNORE_ALL);
-
- if (!$force_ignore) {
- if ($ignore_all && $changeset->getWhitespaceMatters()) {
- $ignore_all = false;
- }
- }
-
- // The "ignore all whitespace" algorithm depends on rediffing the
- // files, and we currently need complete representations of both
- // files to do anything reasonable. If we only have parts of the files,
- // don't use the "ignore all" algorithm.
- if ($ignore_all) {
- $hunks = $changeset->getHunks();
- if (count($hunks) !== 1) {
- $ignore_all = false;
- } else {
- $first_hunk = reset($hunks);
- if ($first_hunk->getOldOffset() != 1 ||
- $first_hunk->getNewOffset() != 1) {
- $ignore_all = false;
- }
- }
- }
-
- if ($ignore_all) {
- $old_file = $changeset->makeOldFile();
- $new_file = $changeset->makeNewFile();
- if ($old_file == $new_file) {
- // If the old and new files are exactly identical, the synthetic
- // diff below will give us nonsense and whitespace modes are
- // irrelevant anyway. This occurs when you, e.g., copy a file onto
- // itself in Subversion (see T271).
- $ignore_all = false;
- }
- }
-
$hunk_parser = new DifferentialHunkParser();
- $hunk_parser->setWhitespaceMode($whitespace_mode);
$hunk_parser->parseHunksForLineData($changeset->getHunks());
- // Depending on the whitespace mode, we may need to compute a different
- // set of changes than the set of changes in the hunk data (specifically,
- // we might want to consider changed lines which have only whitespace
- // changes as unchanged).
- if ($ignore_all) {
- $engine = new PhabricatorDifferenceEngine();
- $engine->setIgnoreWhitespace(true);
- $no_whitespace_changeset = $engine->generateChangesetFromFileContent(
- $old_file,
- $new_file);
-
- $type_parser = new DifferentialHunkParser();
- $type_parser->parseHunksForLineData($no_whitespace_changeset->getHunks());
-
- $hunk_parser->setOldLineTypeMap($type_parser->getOldLineTypeMap());
- $hunk_parser->setNewLineTypeMap($type_parser->getNewLineTypeMap());
- }
+ $this->realignDiff($changeset, $hunk_parser);
$hunk_parser->reparseHunksForSpecialAttributes();
$unchanged = false;
if (!$hunk_parser->getHasAnyChanges()) {
$filetype = $this->changeset->getFileType();
if ($filetype == DifferentialChangeType::FILE_TEXT ||
$filetype == DifferentialChangeType::FILE_SYMLINK) {
$unchanged = true;
}
}
$moveaway = false;
$changetype = $this->changeset->getChangeType();
if ($changetype == DifferentialChangeType::TYPE_MOVE_AWAY) {
$moveaway = true;
}
$this->setSpecialAttributes(array(
self::ATTR_UNCHANGED => $unchanged,
self::ATTR_DELETED => $hunk_parser->getIsDeleted(),
- self::ATTR_WHITELINES => !$hunk_parser->getHasTextChanges(),
self::ATTR_MOVEAWAY => $moveaway,
));
$lines_context = $this->getLinesOfContext();
$hunk_parser->generateIntraLineDiffs();
$hunk_parser->generateVisibileLinesMask($lines_context);
$this->setOldLines($hunk_parser->getOldLines());
$this->setNewLines($hunk_parser->getNewLines());
$this->setIntraLineDiffs($hunk_parser->getIntraLineDiffs());
+ $this->setDepthOnlyLines($hunk_parser->getDepthOnlyLines());
$this->setVisibileLinesMask($hunk_parser->getVisibleLinesMask());
$this->hunkStartLines = $hunk_parser->getHunkStartLines(
$changeset->getHunks());
$new_corpus = $hunk_parser->getNewCorpus();
$new_corpus_block = implode('', $new_corpus);
$this->markGenerated($new_corpus_block);
if ($this->isTopLevel &&
!$this->comments &&
($this->isGenerated() ||
$this->isUnchanged() ||
$this->isDeleted())) {
return;
}
$old_corpus = $hunk_parser->getOldCorpus();
$old_corpus_block = implode('', $old_corpus);
$old_future = $this->getHighlightFuture($old_corpus_block);
$new_future = $this->getHighlightFuture($new_corpus_block);
$futures = array(
'old' => $old_future,
'new' => $new_future,
);
$corpus_blocks = array(
'old' => $old_corpus_block,
'new' => $new_corpus_block,
);
$this->highlightErrors = false;
foreach (new FutureIterator($futures) as $key => $future) {
try {
try {
$highlighted = $future->resolve();
} catch (PhutilSyntaxHighlighterException $ex) {
$this->highlightErrors = true;
$highlighted = id(new PhutilDefaultSyntaxHighlighter())
->getHighlightFuture($corpus_blocks[$key])
->resolve();
}
switch ($key) {
case 'old':
$this->oldRender = $this->processHighlightedSource(
$this->old,
$highlighted);
break;
case 'new':
$this->newRender = $this->processHighlightedSource(
$this->new,
$highlighted);
break;
}
} catch (Exception $ex) {
phlog($ex);
throw $ex;
}
}
$this->applyIntraline(
$this->oldRender,
ipull($this->intra, 0),
$old_corpus);
$this->applyIntraline(
$this->newRender,
ipull($this->intra, 1),
$new_corpus);
}
private function shouldRenderPropertyChangeHeader($changeset) {
if (!$this->isTopLevel) {
// We render properties only at top level; otherwise we get multiple
// copies of them when a user clicks "Show More".
return false;
}
return true;
}
public function render(
$range_start = null,
$range_len = null,
$mask_force = array()) {
// "Top level" renders are initial requests for the whole file, versus
// requests for a specific range generated by clicking "show more". We
// generate property changes and "shield" UI elements only for toplevel
// requests.
$this->isTopLevel = (($range_start === null) && ($range_len === null));
$this->highlightEngine = PhabricatorSyntaxHighlighter::newEngine();
$encoding = null;
if ($this->characterEncoding) {
// We are forcing this changeset to be interpreted with a specific
// character encoding, so force all the hunks into that encoding and
// propagate it to the renderer.
$encoding = $this->characterEncoding;
foreach ($this->changeset->getHunks() as $hunk) {
$hunk->forceEncoding($this->characterEncoding);
}
} else {
// We're just using the default, so tell the renderer what that is
// (by reading the encoding from the first hunk).
foreach ($this->changeset->getHunks() as $hunk) {
$encoding = $hunk->getDataEncoding();
break;
}
}
$this->tryCacheStuff();
// If we're rendering in an offset mode, treat the range numbers as line
// numbers instead of rendering offsets.
$offset_mode = $this->getOffsetMode();
if ($offset_mode) {
if ($offset_mode == 'new') {
$offset_map = $this->new;
} else {
$offset_map = $this->old;
}
// NOTE: Inline comments use zero-based lengths. For example, a comment
// that starts and ends on line 123 has length 0. Rendering considers
// this range to have length 1. Probably both should agree, but that
// ship likely sailed long ago. Tweak things here to get the two systems
// to agree. See PHI985, where this affected mail rendering of inline
// comments left on the final line of a file.
$range_end = $this->getOffset($offset_map, $range_start + $range_len);
$range_start = $this->getOffset($offset_map, $range_start);
$range_len = ($range_end - $range_start) + 1;
}
$render_pch = $this->shouldRenderPropertyChangeHeader($this->changeset);
$rows = max(
count($this->old),
count($this->new));
$renderer = $this->getRenderer()
->setUser($this->getUser())
->setChangeset($this->changeset)
->setRenderPropertyChangeHeader($render_pch)
->setIsTopLevel($this->isTopLevel)
->setOldRender($this->oldRender)
->setNewRender($this->newRender)
->setHunkStartLines($this->hunkStartLines)
->setOldChangesetID($this->leftSideChangesetID)
->setNewChangesetID($this->rightSideChangesetID)
->setOldAttachesToNewFile($this->leftSideAttachesToNewFile)
->setNewAttachesToNewFile($this->rightSideAttachesToNewFile)
->setCodeCoverage($this->getCoverage())
->setRenderingReference($this->getRenderingReference())
->setMarkupEngine($this->markupEngine)
->setHandles($this->handles)
->setOldLines($this->old)
->setNewLines($this->new)
->setOriginalCharacterEncoding($encoding)
->setShowEditAndReplyLinks($this->getShowEditAndReplyLinks())
->setCanMarkDone($this->getCanMarkDone())
->setObjectOwnerPHID($this->getObjectOwnerPHID())
- ->setHighlightingDisabled($this->highlightingDisabled);
+ ->setHighlightingDisabled($this->highlightingDisabled)
+ ->setDepthOnlyLines($this->getDepthOnlyLines());
$shield = null;
if ($this->isTopLevel && !$this->comments) {
if ($this->isGenerated()) {
$shield = $renderer->renderShield(
pht(
'This file contains generated code, which does not normally '.
'need to be reviewed.'));
} else if ($this->isMoveAway()) {
// We put an empty shield on these files. Normally, they do not have
// any diff content anyway. However, if they come through `arc`, they
// may have content. We don't want to show it (it's not useful) and
// we bailed out of fully processing it earlier anyway.
// We could show a message like "this file was moved", but we show
// that as a change header anyway, so it would be redundant. Instead,
// just render an empty shield to skip rendering the diff body.
$shield = '';
} else if ($this->isUnchanged()) {
$type = 'text';
if (!$rows) {
// NOTE: Normally, diffs which don't change files do not include
// file content (for example, if you "chmod +x" a file and then
// run "git show", the file content is not available). Similarly,
// if you move a file from A to B without changing it, diffs normally
// do not show the file content. In some cases `arc` is able to
// synthetically generate content for these diffs, but for raw diffs
// we'll never have it so we need to be prepared to not render a link.
$type = 'none';
}
$type_add = DifferentialChangeType::TYPE_ADD;
if ($this->changeset->getChangeType() == $type_add) {
// Although the generic message is sort of accurate in a technical
// sense, this more-tailored message is less confusing.
$shield = $renderer->renderShield(
pht('This is an empty file.'),
$type);
} else {
$shield = $renderer->renderShield(
pht('The contents of this file were not changed.'),
$type);
}
- } else if ($this->isWhitespaceOnly()) {
- $shield = $renderer->renderShield(
- pht('This file was changed only by adding or removing whitespace.'),
- 'whitespace');
} else if ($this->isDeleted()) {
$shield = $renderer->renderShield(
pht('This file was completely deleted.'));
} else if ($this->changeset->getAffectedLineCount() > 2500) {
$shield = $renderer->renderShield(
pht(
'This file has a very large number of changes (%s lines).',
new PhutilNumber($this->changeset->getAffectedLineCount())));
}
}
if ($shield !== null) {
return $renderer->renderChangesetTable($shield);
}
// This request should render the "undershield" headers if it's a top-level
// request which made it this far (indicating the changeset has no shield)
// or it's a request with no mask information (indicating it's the request
// that removes the rendering shield). Possibly, this second class of
// request might need to be made more explicit.
$is_undershield = (empty($mask_force) || $this->isTopLevel);
$renderer->setIsUndershield($is_undershield);
$old_comments = array();
$new_comments = array();
$old_mask = array();
$new_mask = array();
$feedback_mask = array();
$lines_context = $this->getLinesOfContext();
if ($this->comments) {
// If there are any comments which appear in sections of the file which
// we don't have, we're going to move them backwards to the closest
// earlier line. Two cases where this may happen are:
//
// - Porting ghost comments forward into a file which was mostly
// deleted.
// - Porting ghost comments forward from a full-context diff to a
// partial-context diff.
list($old_backmap, $new_backmap) = $this->buildLineBackmaps();
foreach ($this->comments as $comment) {
$new_side = $this->isCommentOnRightSideWhenDisplayed($comment);
$line = $comment->getLineNumber();
if ($new_side) {
$back_line = $new_backmap[$line];
} else {
$back_line = $old_backmap[$line];
}
if ($back_line != $line) {
// TODO: This should probably be cleaner, but just be simple and
// obvious for now.
$ghost = $comment->getIsGhost();
if ($ghost) {
$moved = pht(
'This comment originally appeared on line %s, but that line '.
'does not exist in this version of the diff. It has been '.
'moved backward to the nearest line.',
new PhutilNumber($line));
$ghost['reason'] = $ghost['reason']."\n\n".$moved;
$comment->setIsGhost($ghost);
}
$comment->setLineNumber($back_line);
$comment->setLineLength(0);
}
$start = max($comment->getLineNumber() - $lines_context, 0);
$end = $comment->getLineNumber() +
$comment->getLineLength() +
$lines_context;
for ($ii = $start; $ii <= $end; $ii++) {
if ($new_side) {
$new_mask[$ii] = true;
} else {
$old_mask[$ii] = true;
}
}
}
foreach ($this->old as $ii => $old) {
if (isset($old['line']) && isset($old_mask[$old['line']])) {
$feedback_mask[$ii] = true;
}
}
foreach ($this->new as $ii => $new) {
if (isset($new['line']) && isset($new_mask[$new['line']])) {
$feedback_mask[$ii] = true;
}
}
$this->comments = id(new PHUIDiffInlineThreader())
->reorderAndThreadCommments($this->comments);
foreach ($this->comments as $comment) {
$final = $comment->getLineNumber() +
$comment->getLineLength();
$final = max(1, $final);
if ($this->isCommentOnRightSideWhenDisplayed($comment)) {
$new_comments[$final][] = $comment;
} else {
$old_comments[$final][] = $comment;
}
}
}
$renderer
->setOldComments($old_comments)
->setNewComments($new_comments);
switch ($this->changeset->getFileType()) {
case DifferentialChangeType::FILE_IMAGE:
$old = null;
$new = null;
// TODO: Improve the architectural issue as discussed in D955
// https://secure.phabricator.com/D955
$reference = $this->getRenderingReference();
$parts = explode('/', $reference);
if (count($parts) == 2) {
list($id, $vs) = $parts;
} else {
$id = $parts[0];
$vs = 0;
}
$id = (int)$id;
$vs = (int)$vs;
if (!$vs) {
$metadata = $this->changeset->getMetadata();
$data = idx($metadata, 'attachment-data');
$old_phid = idx($metadata, 'old:binary-phid');
$new_phid = idx($metadata, 'new:binary-phid');
} else {
$vs_changeset = id(new DifferentialChangeset())->load($vs);
$old_phid = null;
$new_phid = null;
// TODO: This is spooky, see D6851
if ($vs_changeset) {
$vs_metadata = $vs_changeset->getMetadata();
$old_phid = idx($vs_metadata, 'new:binary-phid');
}
$changeset = id(new DifferentialChangeset())->load($id);
if ($changeset) {
$metadata = $changeset->getMetadata();
$new_phid = idx($metadata, 'new:binary-phid');
}
}
if ($old_phid || $new_phid) {
// grab the files, (micro) optimization for 1 query not 2
$file_phids = array();
if ($old_phid) {
$file_phids[] = $old_phid;
}
if ($new_phid) {
$file_phids[] = $new_phid;
}
$files = id(new PhabricatorFileQuery())
->setViewer($this->getUser())
->withPHIDs($file_phids)
->execute();
foreach ($files as $file) {
if (empty($file)) {
continue;
}
if ($file->getPHID() == $old_phid) {
$old = $file;
} else if ($file->getPHID() == $new_phid) {
$new = $file;
}
}
}
$renderer->attachOldFile($old);
$renderer->attachNewFile($new);
return $renderer->renderFileChange($old, $new, $id, $vs);
case DifferentialChangeType::FILE_DIRECTORY:
case DifferentialChangeType::FILE_BINARY:
$output = $renderer->renderChangesetTable(null);
return $output;
}
if ($this->originalLeft && $this->originalRight) {
list($highlight_old, $highlight_new) = $this->diffOriginals();
$highlight_old = array_flip($highlight_old);
$highlight_new = array_flip($highlight_new);
$renderer
->setHighlightOld($highlight_old)
->setHighlightNew($highlight_new);
}
$renderer
->setOriginalOld($this->originalLeft)
->setOriginalNew($this->originalRight);
if ($range_start === null) {
$range_start = 0;
}
if ($range_len === null) {
$range_len = $rows;
}
$range_len = min($range_len, $rows - $range_start);
- list($gaps, $mask, $depths) = $this->calculateGapsMaskAndDepths(
+ list($gaps, $mask) = $this->calculateGapsAndMask(
$mask_force,
$feedback_mask,
$range_start,
$range_len);
$renderer
->setGaps($gaps)
- ->setMask($mask)
- ->setDepths($depths);
+ ->setMask($mask);
$html = $renderer->renderTextChange(
$range_start,
$range_len,
$rows);
return $renderer->renderChangesetTable($html);
}
/**
* This function calculates a lot of stuff we need to know to display
* the diff:
*
* Gaps - compute gaps in the visible display diff, where we will render
* "Show more context" spacers. If a gap is smaller than the context size,
* we just display it. Otherwise, we record it into $gaps and will render a
* "show more context" element instead of diff text below. A given $gap
* is a tuple of $gap_line_number_start and $gap_length.
*
* Mask - compute the actual lines that need to be shown (because they
* are near changes lines, near inline comments, or the request has
* explicitly asked for them, i.e. resulting from the user clicking
* "show more"). The $mask returned is a sparsely populated dictionary
* of $visible_line_number => true.
*
- * Depths - compute how indented any given line is. The $depths returned
- * is a sparsely populated dictionary of $visible_line_number => $depth.
- *
- * This function also has the side effect of modifying member variable
- * new such that tabs are normalized to spaces for each line of the diff.
- *
- * @return array($gaps, $mask, $depths)
+ * @return array($gaps, $mask)
*/
- private function calculateGapsMaskAndDepths(
+ private function calculateGapsAndMask(
$mask_force,
$feedback_mask,
$range_start,
$range_len) {
$lines_context = $this->getLinesOfContext();
- // Calculate gaps and mask first
$gaps = array();
$gap_start = 0;
$in_gap = false;
$base_mask = $this->visible + $mask_force + $feedback_mask;
$base_mask[$range_start + $range_len] = true;
for ($ii = $range_start; $ii <= $range_start + $range_len; $ii++) {
if (isset($base_mask[$ii])) {
if ($in_gap) {
$gap_length = $ii - $gap_start;
if ($gap_length <= $lines_context) {
for ($jj = $gap_start; $jj <= $gap_start + $gap_length; $jj++) {
$base_mask[$jj] = true;
}
} else {
$gaps[] = array($gap_start, $gap_length);
}
$in_gap = false;
}
} else {
if (!$in_gap) {
$gap_start = $ii;
$in_gap = true;
}
}
}
$gaps = array_reverse($gaps);
$mask = $base_mask;
- // Time to calculate depth.
- // We need to go backwards to properly indent whitespace in this code:
- //
- // 0: class C {
- // 1:
- // 1: function f() {
- // 2:
- // 2: return;
- // 1:
- // 1: }
- // 0:
- // 0: }
- //
- $depths = array();
- $last_depth = 0;
- $range_end = $range_start + $range_len;
- if (!isset($this->new[$range_end])) {
- $range_end--;
- }
- for ($ii = $range_end; $ii >= $range_start; $ii--) {
- // We need to expand tabs to process mixed indenting and to round
- // correctly later.
- $line = str_replace("\t", ' ', $this->new[$ii]['text']);
- $trimmed = ltrim($line);
- if ($trimmed != '') {
- // We round down to flatten "/**" and " *".
- $last_depth = floor((strlen($line) - strlen($trimmed)) / 2);
- }
- $depths[$ii] = $last_depth;
- }
-
- return array($gaps, $mask, $depths);
+ return array($gaps, $mask);
}
/**
* Determine if an inline comment will appear on the rendered diff,
* taking into consideration which halves of which changesets will actually
* be shown.
*
* @param PhabricatorInlineCommentInterface Comment to test for visibility.
* @return bool True if the comment is visible on the rendered diff.
*/
private function isCommentVisibleOnRenderedDiff(
PhabricatorInlineCommentInterface $comment) {
$changeset_id = $comment->getChangesetID();
$is_new = $comment->getIsNewFile();
if ($changeset_id == $this->rightSideChangesetID &&
$is_new == $this->rightSideAttachesToNewFile) {
return true;
}
if ($changeset_id == $this->leftSideChangesetID &&
$is_new == $this->leftSideAttachesToNewFile) {
return true;
}
return false;
}
/**
* Determine if a comment will appear on the right side of the display diff.
* Note that the comment must appear somewhere on the rendered changeset, as
* per isCommentVisibleOnRenderedDiff().
*
* @param PhabricatorInlineCommentInterface Comment to test for display
* location.
* @return bool True for right, false for left.
*/
private function isCommentOnRightSideWhenDisplayed(
PhabricatorInlineCommentInterface $comment) {
if (!$this->isCommentVisibleOnRenderedDiff($comment)) {
throw new Exception(pht('Comment is not visible on changeset!'));
}
$changeset_id = $comment->getChangesetID();
$is_new = $comment->getIsNewFile();
if ($changeset_id == $this->rightSideChangesetID &&
$is_new == $this->rightSideAttachesToNewFile) {
return true;
}
return false;
}
/**
* Parse the 'range' specification that this class and the client-side JS
* emit to indicate that a user clicked "Show more..." on a diff. Generally,
* use is something like this:
*
* $spec = $request->getStr('range');
* $parsed = DifferentialChangesetParser::parseRangeSpecification($spec);
* list($start, $end, $mask) = $parsed;
* $parser->render($start, $end, $mask);
*
* @param string Range specification, indicating the range of the diff that
* should be rendered.
* @return tuple List of <start, end, mask> suitable for passing to
* @{method:render}.
*/
public static function parseRangeSpecification($spec) {
$range_s = null;
$range_e = null;
$mask = array();
if ($spec) {
$match = null;
if (preg_match('@^(\d+)-(\d+)(?:/(\d+)-(\d+))?$@', $spec, $match)) {
$range_s = (int)$match[1];
$range_e = (int)$match[2];
if (count($match) > 3) {
$start = (int)$match[3];
$len = (int)$match[4];
for ($ii = $start; $ii < $start + $len; $ii++) {
$mask[$ii] = true;
}
}
}
}
return array($range_s, $range_e, $mask);
}
/**
* Render "modified coverage" information; test coverage on modified lines.
* This synthesizes diff information with unit test information into a useful
* indicator of how well tested a change is.
*/
public function renderModifiedCoverage() {
$na = phutil_tag('em', array(), '-');
$coverage = $this->getCoverage();
if (!$coverage) {
return $na;
}
$covered = 0;
$not_covered = 0;
foreach ($this->new as $k => $new) {
if (!$new['line']) {
continue;
}
if (!$new['type']) {
continue;
}
if (empty($coverage[$new['line'] - 1])) {
continue;
}
switch ($coverage[$new['line'] - 1]) {
case 'C':
$covered++;
break;
case 'U':
$not_covered++;
break;
}
}
if (!$covered && !$not_covered) {
return $na;
}
return sprintf('%d%%', 100 * ($covered / ($covered + $not_covered)));
}
/**
* Build maps from lines comments appear on to actual lines.
*/
private function buildLineBackmaps() {
$old_back = array();
$new_back = array();
foreach ($this->old as $ii => $old) {
$old_back[$old['line']] = $old['line'];
}
foreach ($this->new as $ii => $new) {
$new_back[$new['line']] = $new['line'];
}
$max_old_line = 0;
$max_new_line = 0;
foreach ($this->comments as $comment) {
if ($this->isCommentOnRightSideWhenDisplayed($comment)) {
$max_new_line = max($max_new_line, $comment->getLineNumber());
} else {
$max_old_line = max($max_old_line, $comment->getLineNumber());
}
}
$cursor = 1;
for ($ii = 1; $ii <= $max_old_line; $ii++) {
if (empty($old_back[$ii])) {
$old_back[$ii] = $cursor;
} else {
$cursor = $old_back[$ii];
}
}
$cursor = 1;
for ($ii = 1; $ii <= $max_new_line; $ii++) {
if (empty($new_back[$ii])) {
$new_back[$ii] = $cursor;
} else {
$cursor = $new_back[$ii];
}
}
return array($old_back, $new_back);
}
private function getOffset(array $map, $line) {
if (!$map) {
return null;
}
$line = (int)$line;
foreach ($map as $key => $spec) {
if ($spec && isset($spec['line'])) {
if ((int)$spec['line'] >= $line) {
return $key;
}
}
}
return $key;
}
+ private function realignDiff(
+ DifferentialChangeset $changeset,
+ DifferentialHunkParser $hunk_parser) {
+ // Normalizing and realigning the diff depends on rediffing the files, and
+ // we currently need complete representations of both files to do anything
+ // reasonable. If we only have parts of the files, skip realignment.
+
+ // We have more than one hunk, so we're definitely missing part of the file.
+ $hunks = $changeset->getHunks();
+ if (count($hunks) !== 1) {
+ return null;
+ }
+
+ // The first hunk doesn't start at the beginning of the file, so we're
+ // missing some context.
+ $first_hunk = head($hunks);
+ if ($first_hunk->getOldOffset() != 1 || $first_hunk->getNewOffset() != 1) {
+ return null;
+ }
+
+ $old_file = $changeset->makeOldFile();
+ $new_file = $changeset->makeNewFile();
+ if ($old_file === $new_file) {
+ // If the old and new files are exactly identical, the synthetic
+ // diff below will give us nonsense and whitespace modes are
+ // irrelevant anyway. This occurs when you, e.g., copy a file onto
+ // itself in Subversion (see T271).
+ return null;
+ }
+
+
+ $engine = id(new PhabricatorDifferenceEngine())
+ ->setNormalize(true);
+
+ $normalized_changeset = $engine->generateChangesetFromFileContent(
+ $old_file,
+ $new_file);
+
+ $type_parser = new DifferentialHunkParser();
+ $type_parser->parseHunksForLineData($normalized_changeset->getHunks());
+
+ $hunk_parser->setNormalized(true);
+ $hunk_parser->setOldLineTypeMap($type_parser->getOldLineTypeMap());
+ $hunk_parser->setNewLineTypeMap($type_parser->getNewLineTypeMap());
+ }
+
+ private function adjustRenderedLineForDisplay($line) {
+ // IMPORTANT: We're using "str_replace()" against raw HTML here, which can
+ // easily become unsafe. The input HTML has already had syntax highlighting
+ // and intraline diff highlighting applied, so it's full of "<span />" tags.
+
+ static $search;
+ static $replace;
+ if ($search === null) {
+ $rules = $this->newSuspiciousCharacterRules();
+
+ $map = array();
+ foreach ($rules as $key => $spec) {
+ $tag = phutil_tag(
+ 'span',
+ array(
+ 'data-copy-text' => $key,
+ 'class' => $spec['class'],
+ 'title' => $spec['title'],
+ ),
+ $spec['replacement']);
+ $map[$key] = phutil_string_cast($tag);
+ }
+
+ $search = array_keys($map);
+ $replace = array_values($map);
+ }
+
+ $is_html = false;
+ if ($line instanceof PhutilSafeHTML) {
+ $is_html = true;
+ $line = hsprintf('%s', $line);
+ }
+
+ $line = phutil_string_cast($line);
+
+ if (strpos($line, "\t") !== false) {
+ $line = $this->replaceTabsWithSpaces($line);
+ }
+ $line = str_replace($search, $replace, $line);
+
+ if ($is_html) {
+ $line = phutil_safe_html($line);
+ }
+
+ return $line;
+ }
+
+ private function newSuspiciousCharacterRules() {
+ // The "title" attributes are cached in the database, so they're
+ // intentionally not wrapped in "pht(...)".
+
+ $rules = array(
+ "\xE2\x80\x8B" => array(
+ 'title' => 'ZWS',
+ 'class' => 'suspicious-character',
+ 'replacement' => '!',
+ ),
+ "\xC2\xA0" => array(
+ 'title' => 'NBSP',
+ 'class' => 'suspicious-character',
+ 'replacement' => '!',
+ ),
+ "\x7F" => array(
+ 'title' => 'DEL (0x7F)',
+ 'class' => 'suspicious-character',
+ 'replacement' => "\xE2\x90\xA1",
+ ),
+ );
+
+ // Unicode defines special pictures for the control characters in the
+ // range between "0x00" and "0x1F".
+
+ $control = array(
+ 'NULL',
+ 'SOH',
+ 'STX',
+ 'ETX',
+ 'EOT',
+ 'ENQ',
+ 'ACK',
+ 'BEL',
+ 'BS',
+ null, // "\t" Tab
+ null, // "\n" New Line
+ 'VT',
+ 'FF',
+ null, // "\r" Carriage Return,
+ 'SO',
+ 'SI',
+ 'DLE',
+ 'DC1',
+ 'DC2',
+ 'DC3',
+ 'DC4',
+ 'NAK',
+ 'SYN',
+ 'ETB',
+ 'CAN',
+ 'EM',
+ 'SUB',
+ 'ESC',
+ 'FS',
+ 'GS',
+ 'RS',
+ 'US',
+ );
+
+ foreach ($control as $idx => $label) {
+ if ($label === null) {
+ continue;
+ }
+
+ $rules[chr($idx)] = array(
+ 'title' => sprintf('%s (0x%02X)', $label, $idx),
+ 'class' => 'suspicious-character',
+ 'replacement' => "\xE2\x90".chr(0x80 + $idx),
+ );
+ }
+
+ return $rules;
+ }
+
+ private function replaceTabsWithSpaces($line) {
+ // TODO: This should be flexible, eventually.
+ $tab_width = 2;
+
+ static $tags;
+ if ($tags === null) {
+ $tags = array();
+ for ($ii = 1; $ii <= $tab_width; $ii++) {
+ $tag = phutil_tag(
+ 'span',
+ array(
+ 'data-copy-text' => "\t",
+ ),
+ str_repeat(' ', $ii));
+ $tag = phutil_string_cast($tag);
+ $tags[$ii] = $tag;
+ }
+ }
+
+ // If the line is particularly long, don't try to vectorize it. Use a
+ // faster approximation of the correct tabstop expansion instead. This
+ // usually still arrives at the right result.
+ if (strlen($line) > 256) {
+ return str_replace("\t", $tags[$tab_width], $line);
+ }
+
+ $line = phutil_utf8v_combined($line);
+ $in_tag = false;
+ $pos = 0;
+ foreach ($line as $key => $char) {
+ if ($char === '<') {
+ $in_tag = true;
+ continue;
+ }
+
+ if ($char === '>') {
+ $in_tag = false;
+ continue;
+ }
+
+ if ($in_tag) {
+ continue;
+ }
+
+ if ($char === "\t") {
+ $count = $tab_width - ($pos % $tab_width);
+ $pos += $count;
+ $line[$key] = $tags[$count];
+ continue;
+ }
+
+ $pos++;
+ }
+
+ return implode('', $line);
+ }
+
}
diff --git a/src/applications/differential/parser/DifferentialHunkParser.php b/src/applications/differential/parser/DifferentialHunkParser.php
index 5bd98e901..8667c032b 100644
--- a/src/applications/differential/parser/DifferentialHunkParser.php
+++ b/src/applications/differential/parser/DifferentialHunkParser.php
@@ -1,674 +1,819 @@
<?php
final class DifferentialHunkParser extends Phobject {
private $oldLines;
private $newLines;
private $intraLineDiffs;
+ private $depthOnlyLines;
private $visibleLinesMask;
- private $whitespaceMode;
+ private $normalized;
/**
* Get a map of lines on which hunks start, other than line 1. This
* datastructure is used to determine when to render "Context not available."
* in diffs with multiple hunks.
*
* @return dict<int, bool> Map of lines where hunks start, other than line 1.
*/
public function getHunkStartLines(array $hunks) {
assert_instances_of($hunks, 'DifferentialHunk');
$map = array();
foreach ($hunks as $hunk) {
$line = $hunk->getOldOffset();
if ($line > 1) {
$map[$line] = true;
}
}
return $map;
}
private function setVisibleLinesMask($mask) {
$this->visibleLinesMask = $mask;
return $this;
}
public function getVisibleLinesMask() {
if ($this->visibleLinesMask === null) {
throw new PhutilInvalidStateException('generateVisibileLinesMask');
}
return $this->visibleLinesMask;
}
private function setIntraLineDiffs($intra_line_diffs) {
$this->intraLineDiffs = $intra_line_diffs;
return $this;
}
public function getIntraLineDiffs() {
if ($this->intraLineDiffs === null) {
throw new PhutilInvalidStateException('generateIntraLineDiffs');
}
return $this->intraLineDiffs;
}
private function setNewLines($new_lines) {
$this->newLines = $new_lines;
return $this;
}
public function getNewLines() {
if ($this->newLines === null) {
throw new PhutilInvalidStateException('parseHunksForLineData');
}
return $this->newLines;
}
private function setOldLines($old_lines) {
$this->oldLines = $old_lines;
return $this;
}
public function getOldLines() {
if ($this->oldLines === null) {
throw new PhutilInvalidStateException('parseHunksForLineData');
}
return $this->oldLines;
}
public function getOldLineTypeMap() {
$map = array();
$old = $this->getOldLines();
foreach ($old as $o) {
if (!$o) {
continue;
}
$map[$o['line']] = $o['type'];
}
return $map;
}
public function setOldLineTypeMap(array $map) {
$lines = $this->getOldLines();
foreach ($lines as $key => $data) {
$lines[$key]['type'] = idx($map, $data['line']);
}
$this->oldLines = $lines;
return $this;
}
public function getNewLineTypeMap() {
$map = array();
$new = $this->getNewLines();
foreach ($new as $n) {
if (!$n) {
continue;
}
$map[$n['line']] = $n['type'];
}
return $map;
}
public function setNewLineTypeMap(array $map) {
$lines = $this->getNewLines();
foreach ($lines as $key => $data) {
$lines[$key]['type'] = idx($map, $data['line']);
}
$this->newLines = $lines;
return $this;
}
+ public function setDepthOnlyLines(array $map) {
+ $this->depthOnlyLines = $map;
+ return $this;
+ }
+
+ public function getDepthOnlyLines() {
+ return $this->depthOnlyLines;
+ }
- public function setWhitespaceMode($white_space_mode) {
- $this->whitespaceMode = $white_space_mode;
+ public function setNormalized($normalized) {
+ $this->normalized = $normalized;
return $this;
}
- private function getWhitespaceMode() {
- if ($this->whitespaceMode === null) {
- throw new Exception(
- pht(
- 'You must %s before accessing this data.',
- 'setWhitespaceMode'));
- }
- return $this->whitespaceMode;
+ public function getNormalized() {
+ return $this->normalized;
}
public function getIsDeleted() {
foreach ($this->getNewLines() as $line) {
if ($line) {
// At least one new line, so the entire file wasn't deleted.
return false;
}
}
foreach ($this->getOldLines() as $line) {
if ($line) {
// No new lines, at least one old line; the entire file was deleted.
return true;
}
}
// This is an empty file.
return false;
}
- /**
- * Returns true if the hunks change any text, not just whitespace.
- */
- public function getHasTextChanges() {
- return $this->getHasChanges('text');
- }
-
/**
* Returns true if the hunks change anything, including whitespace.
*/
public function getHasAnyChanges() {
return $this->getHasChanges('any');
}
private function getHasChanges($filter) {
if ($filter !== 'any' && $filter !== 'text') {
throw new Exception(pht("Unknown change filter '%s'.", $filter));
}
$old = $this->getOldLines();
$new = $this->getNewLines();
$is_any = ($filter === 'any');
foreach ($old as $key => $o) {
$n = $new[$key];
if ($o === null || $n === null) {
// One side is missing, and it's impossible for both sides to be null,
// so the other side must have something, and thus the two sides are
// different and the file has been changed under any type of filter.
return true;
}
if ($o['type'] !== $n['type']) {
- // The types are different, so either the underlying text is actually
- // different or whatever whitespace rules we're using consider them
- // different.
return true;
}
if ($o['text'] !== $n['text']) {
if ($is_any) {
// The text is different, so there's a change.
return true;
} else if (trim($o['text']) !== trim($n['text'])) {
return true;
}
}
}
// No changes anywhere in the file.
return false;
}
/**
* This function takes advantage of the parsing work done in
* @{method:parseHunksForLineData} and continues the struggle to hammer this
* data into something we can display to a user.
*
* In particular, this function re-parses the hunks to make them equivalent
* in length for easy rendering, adding `null` as necessary to pad the
* length.
*
* Anyhoo, this function is not particularly well-named but I try.
*
* NOTE: this function must be called after
* @{method:parseHunksForLineData}.
*/
public function reparseHunksForSpecialAttributes() {
$rebuild_old = array();
$rebuild_new = array();
$old_lines = array_reverse($this->getOldLines());
$new_lines = array_reverse($this->getNewLines());
while (count($old_lines) || count($new_lines)) {
$old_line_data = array_pop($old_lines);
$new_line_data = array_pop($new_lines);
if ($old_line_data) {
$o_type = $old_line_data['type'];
} else {
$o_type = null;
}
if ($new_line_data) {
$n_type = $new_line_data['type'];
} else {
$n_type = null;
}
// This line does not exist in the new file.
if (($o_type != null) && ($n_type == null)) {
$rebuild_old[] = $old_line_data;
$rebuild_new[] = null;
if ($new_line_data) {
array_push($new_lines, $new_line_data);
}
continue;
}
// This line does not exist in the old file.
if (($n_type != null) && ($o_type == null)) {
$rebuild_old[] = null;
$rebuild_new[] = $new_line_data;
if ($old_line_data) {
array_push($old_lines, $old_line_data);
}
continue;
}
$rebuild_old[] = $old_line_data;
$rebuild_new[] = $new_line_data;
}
$this->setOldLines($rebuild_old);
$this->setNewLines($rebuild_new);
- $this->updateChangeTypesForWhitespaceMode();
+ $this->updateChangeTypesForNormalization();
return $this;
}
- private function updateChangeTypesForWhitespaceMode() {
- $mode = $this->getWhitespaceMode();
-
- $mode_show_all = DifferentialChangesetParser::WHITESPACE_SHOW_ALL;
- if ($mode === $mode_show_all) {
- // If we're showing all whitespace, we don't need to perform any updates.
- return;
- }
-
- $mode_trailing = DifferentialChangesetParser::WHITESPACE_IGNORE_TRAILING;
- $is_trailing = ($mode === $mode_trailing);
-
- $new = $this->getNewLines();
+ public function generateIntraLineDiffs() {
$old = $this->getOldLines();
+ $new = $this->getNewLines();
+
+ $diffs = array();
+ $depth_only = array();
foreach ($old as $key => $o) {
$n = $new[$key];
if (!$o || !$n) {
continue;
}
- if ($is_trailing) {
- // In "trailing" mode, we need to identify lines which are marked
- // changed but differ only by trailing whitespace. We mark these lines
- // unchanged.
- if ($o['type'] != $n['type']) {
- if (rtrim($o['text']) === rtrim($n['text'])) {
- $old[$key]['type'] = null;
- $new[$key]['type'] = null;
- }
- }
- } else {
- // In "ignore most" and "ignore all" modes, we need to identify lines
- // which are marked unchanged but have internal whitespace changes.
- // We want to ignore leading and trailing whitespace changes only, not
- // internal whitespace changes (`diff` doesn't have a mode for this, so
- // we have to fix it here). If the text is marked unchanged but the
- // old and new text differs by internal space, mark the lines changed.
- if ($o['type'] === null && $n['type'] === null) {
- if ($o['text'] !== $n['text']) {
- if (trim($o['text']) !== trim($n['text'])) {
- $old[$key]['type'] = '-';
- $new[$key]['type'] = '+';
+ if ($o['type'] != $n['type']) {
+ $o_segments = array();
+ $n_segments = array();
+ $tab_width = 2;
+
+ $o_text = $o['text'];
+ $n_text = $n['text'];
+
+ if ($o_text !== $n_text && (ltrim($o_text) === ltrim($n_text))) {
+ $o_depth = $this->getIndentDepth($o_text, $tab_width);
+ $n_depth = $this->getIndentDepth($n_text, $tab_width);
+
+ if ($o_depth < $n_depth) {
+ $segment_type = '>';
+ $segment_width = $this->getCharacterCountForVisualWhitespace(
+ $n_text,
+ ($n_depth - $o_depth),
+ $tab_width);
+ if ($segment_width) {
+ $n_text = substr($n_text, $segment_width);
+ $n_segments[] = array(
+ $segment_type,
+ $segment_width,
+ );
+ }
+ } else if ($o_depth > $n_depth) {
+ $segment_type = '<';
+ $segment_width = $this->getCharacterCountForVisualWhitespace(
+ $o_text,
+ ($o_depth - $n_depth),
+ $tab_width);
+ if ($segment_width) {
+ $o_text = substr($o_text, $segment_width);
+ $o_segments[] = array(
+ $segment_type,
+ $segment_width,
+ );
}
}
- }
- }
- }
- $this->setOldLines($old);
- $this->setNewLines($new);
+ // If there are no remaining changes to this line after we've marked
+ // off the indent depth changes, this line was only modified by
+ // changing the indent depth. Mark it for later so we can change how
+ // it is displayed.
+ if ($o_text === $n_text) {
+ $depth_only[$key] = $segment_type;
+ }
+ }
- return $this;
- }
+ $intraline_segments = ArcanistDiffUtils::generateIntralineDiff(
+ $o_text,
+ $n_text);
- public function generateIntraLineDiffs() {
- $old = $this->getOldLines();
- $new = $this->getNewLines();
-
- $diffs = array();
- foreach ($old as $key => $o) {
- $n = $new[$key];
+ foreach ($intraline_segments[0] as $o_segment) {
+ $o_segments[] = $o_segment;
+ }
- if (!$o || !$n) {
- continue;
- }
+ foreach ($intraline_segments[1] as $n_segment) {
+ $n_segments[] = $n_segment;
+ }
- if ($o['type'] != $n['type']) {
- $diffs[$key] = ArcanistDiffUtils::generateIntralineDiff(
- $o['text'],
- $n['text']);
+ $diffs[$key] = array(
+ $o_segments,
+ $n_segments,
+ );
}
}
$this->setIntraLineDiffs($diffs);
+ $this->setDepthOnlyLines($depth_only);
return $this;
}
public function generateVisibileLinesMask($lines_context) {
$old = $this->getOldLines();
$new = $this->getNewLines();
$max_length = max(count($old), count($new));
$visible = false;
$last = 0;
$mask = array();
for ($cursor = -$lines_context; $cursor < $max_length; $cursor++) {
$offset = $cursor + $lines_context;
if ((isset($old[$offset]) && $old[$offset]['type']) ||
(isset($new[$offset]) && $new[$offset]['type'])) {
$visible = true;
$last = $offset;
} else if ($cursor > $last + $lines_context) {
$visible = false;
}
if ($visible && $cursor > 0) {
$mask[$cursor] = 1;
}
}
$this->setVisibleLinesMask($mask);
return $this;
}
public function getOldCorpus() {
return $this->getCorpus($this->getOldLines());
}
public function getNewCorpus() {
return $this->getCorpus($this->getNewLines());
}
private function getCorpus(array $lines) {
$corpus = array();
foreach ($lines as $l) {
if ($l['type'] != '\\') {
if ($l['text'] === null) {
// There's no text on this side of the diff, but insert a placeholder
// newline so the highlighted line numbers match up.
$corpus[] = "\n";
} else {
$corpus[] = $l['text'];
}
}
}
return $corpus;
}
public function parseHunksForLineData(array $hunks) {
assert_instances_of($hunks, 'DifferentialHunk');
$old_lines = array();
$new_lines = array();
foreach ($hunks as $hunk) {
$lines = $hunk->getSplitLines();
$line_type_map = array();
$line_text = array();
foreach ($lines as $line_index => $line) {
if (isset($line[0])) {
$char = $line[0];
switch ($char) {
case ' ':
$line_type_map[$line_index] = null;
$line_text[$line_index] = substr($line, 1);
break;
case "\r":
case "\n":
// NOTE: Normally, the first character is a space, plus, minus or
// backslash, but it may be a newline if it used to be a space and
// trailing whitespace has been stripped via email transmission or
// some similar mechanism. In these cases, we essentially pretend
// the missing space is still there.
$line_type_map[$line_index] = null;
$line_text[$line_index] = $line;
break;
case '+':
case '-':
case '\\':
$line_type_map[$line_index] = $char;
$line_text[$line_index] = substr($line, 1);
break;
default:
throw new Exception(
pht(
'Unexpected leading character "%s" at line index %s!',
$char,
$line_index));
}
} else {
$line_type_map[$line_index] = null;
$line_text[$line_index] = '';
}
}
$old_line = $hunk->getOldOffset();
$new_line = $hunk->getNewOffset();
$num_lines = count($lines);
for ($cursor = 0; $cursor < $num_lines; $cursor++) {
$type = $line_type_map[$cursor];
$data = array(
'type' => $type,
'text' => $line_text[$cursor],
'line' => $new_line,
);
if ($type == '\\') {
$type = $line_type_map[$cursor - 1];
$data['text'] = ltrim($data['text']);
}
switch ($type) {
case '+':
$new_lines[] = $data;
++$new_line;
break;
case '-':
$data['line'] = $old_line;
$old_lines[] = $data;
++$old_line;
break;
default:
$new_lines[] = $data;
$data['line'] = $old_line;
$old_lines[] = $data;
++$new_line;
++$old_line;
break;
}
}
}
$this->setOldLines($old_lines);
$this->setNewLines($new_lines);
return $this;
}
public function parseHunksForHighlightMasks(
array $changeset_hunks,
array $old_hunks,
array $new_hunks) {
assert_instances_of($changeset_hunks, 'DifferentialHunk');
assert_instances_of($old_hunks, 'DifferentialHunk');
assert_instances_of($new_hunks, 'DifferentialHunk');
// Put changes side by side.
$olds = array();
$news = array();
$olds_cursor = -1;
$news_cursor = -1;
foreach ($changeset_hunks as $hunk) {
$n_old = $hunk->getOldOffset();
$n_new = $hunk->getNewOffset();
$changes = $hunk->getSplitLines();
foreach ($changes as $line) {
$diff_type = $line[0]; // Change type in diff of diffs.
$orig_type = $line[1]; // Change type in the original diff.
if ($diff_type == ' ') {
// Use the same key for lines that are next to each other.
if ($olds_cursor > $news_cursor) {
$key = $olds_cursor + 1;
} else {
$key = $news_cursor + 1;
}
$olds[$key] = null;
$news[$key] = null;
$olds_cursor = $key;
$news_cursor = $key;
} else if ($diff_type == '-') {
$olds[] = array($n_old, $orig_type);
$olds_cursor++;
} else if ($diff_type == '+') {
$news[] = array($n_new, $orig_type);
$news_cursor++;
}
if (($diff_type == '-' || $diff_type == ' ') && $orig_type != '-') {
$n_old++;
}
if (($diff_type == '+' || $diff_type == ' ') && $orig_type != '-') {
$n_new++;
}
}
}
$offsets_old = $this->computeOffsets($old_hunks);
$offsets_new = $this->computeOffsets($new_hunks);
// Highlight lines that were added on each side or removed on the other
// side.
$highlight_old = array();
$highlight_new = array();
$last = max(last_key($olds), last_key($news));
for ($i = 0; $i <= $last; $i++) {
if (isset($olds[$i])) {
list($n, $type) = $olds[$i];
if ($type == '+' ||
($type == ' ' && isset($news[$i]) && $news[$i][1] != ' ')) {
$highlight_old[] = $offsets_old[$n];
}
}
if (isset($news[$i])) {
list($n, $type) = $news[$i];
if ($type == '+' ||
($type == ' ' && isset($olds[$i]) && $olds[$i][1] != ' ')) {
$highlight_new[] = $offsets_new[$n];
}
}
}
return array($highlight_old, $highlight_new);
}
public function makeContextDiff(
array $hunks,
$is_new,
$line_number,
$line_length,
$add_context) {
assert_instances_of($hunks, 'DifferentialHunk');
$context = array();
if ($is_new) {
$prefix = '+';
} else {
$prefix = '-';
}
foreach ($hunks as $hunk) {
if ($is_new) {
$offset = $hunk->getNewOffset();
$length = $hunk->getNewLen();
} else {
$offset = $hunk->getOldOffset();
$length = $hunk->getOldLen();
}
$start = $line_number - $offset;
$end = $start + $line_length;
// We need to go in if $start == $length, because the last line
// might be a "\No newline at end of file" marker, which we want
// to show if the additional context is > 0.
if ($start <= $length && $end >= 0) {
$start = $start - $add_context;
$end = $end + $add_context;
$hunk_content = array();
$hunk_pos = array('-' => 0, '+' => 0);
$hunk_offset = array('-' => null, '+' => null);
$hunk_last = array('-' => null, '+' => null);
foreach (explode("\n", $hunk->getChanges()) as $line) {
$in_common = strncmp($line, ' ', 1) === 0;
$in_old = strncmp($line, '-', 1) === 0 || $in_common;
$in_new = strncmp($line, '+', 1) === 0 || $in_common;
$in_selected = strncmp($line, $prefix, 1) === 0;
$skip = !$in_selected && !$in_common;
if ($hunk_pos[$prefix] <= $end) {
if ($start <= $hunk_pos[$prefix]) {
if (!$skip || ($hunk_pos[$prefix] != $start &&
$hunk_pos[$prefix] != $end)) {
if ($in_old) {
if ($hunk_offset['-'] === null) {
$hunk_offset['-'] = $hunk_pos['-'];
}
$hunk_last['-'] = $hunk_pos['-'];
}
if ($in_new) {
if ($hunk_offset['+'] === null) {
$hunk_offset['+'] = $hunk_pos['+'];
}
$hunk_last['+'] = $hunk_pos['+'];
}
$hunk_content[] = $line;
}
}
if ($in_old) { ++$hunk_pos['-']; }
if ($in_new) { ++$hunk_pos['+']; }
}
}
if ($hunk_offset['-'] !== null || $hunk_offset['+'] !== null) {
$header = '@@';
if ($hunk_offset['-'] !== null) {
$header .= ' -'.($hunk->getOldOffset() + $hunk_offset['-']).
','.($hunk_last['-'] - $hunk_offset['-'] + 1);
}
if ($hunk_offset['+'] !== null) {
$header .= ' +'.($hunk->getNewOffset() + $hunk_offset['+']).
','.($hunk_last['+'] - $hunk_offset['+'] + 1);
}
$header .= ' @@';
$context[] = $header;
$context[] = implode("\n", $hunk_content);
}
}
}
return implode("\n", $context);
}
private function computeOffsets(array $hunks) {
assert_instances_of($hunks, 'DifferentialHunk');
$offsets = array();
$n = 1;
foreach ($hunks as $hunk) {
$new_length = $hunk->getNewLen();
$new_offset = $hunk->getNewOffset();
for ($i = 0; $i < $new_length; $i++) {
$offsets[$n] = $new_offset + $i;
$n++;
}
}
return $offsets;
}
+
+ private function getIndentDepth($text, $tab_width) {
+ $len = strlen($text);
+
+ $depth = 0;
+ for ($ii = 0; $ii < $len; $ii++) {
+ $c = $text[$ii];
+
+ // If this is a space, increase the indent depth by 1.
+ if ($c == ' ') {
+ $depth++;
+ continue;
+ }
+
+ // If this is a tab, increase the indent depth to the next tabstop.
+
+ // For example, if the tab width is 4, these sequences both lead us to
+ // a visual width of 8, i.e. the cursor will be in the 8th column:
+ //
+ // <tab><tab>
+ // <space><tab><space><space><space><tab>
+
+ if ($c == "\t") {
+ $depth = ($depth + $tab_width);
+ $depth = $depth - ($depth % $tab_width);
+ continue;
+ }
+
+ break;
+ }
+
+ return $depth;
+ }
+
+ private function getCharacterCountForVisualWhitespace(
+ $text,
+ $depth,
+ $tab_width) {
+
+ // Here, we know the visual indent depth of a line has been increased by
+ // some amount (for example, 6 characters).
+
+ // We want to find the largest whitespace prefix of the string we can
+ // which still fits into that amount of visual space.
+
+ // In most cases, this is very easy. For example, if the string has been
+ // indented by two characters and the string begins with two spaces, that's
+ // a perfect match.
+
+ // However, if the string has been indented by 7 characters, the tab width
+ // is 8, and the string begins with "<space><space><tab>", we can only
+ // mark the two spaces as an indent change. These cases are unusual.
+
+ $character_depth = 0;
+ $visual_depth = 0;
+
+ $len = strlen($text);
+ for ($ii = 0; $ii < $len; $ii++) {
+ if ($visual_depth >= $depth) {
+ break;
+ }
+
+ $c = $text[$ii];
+
+ if ($c == ' ') {
+ $character_depth++;
+ $visual_depth++;
+ continue;
+ }
+
+ if ($c == "\t") {
+ // Figure out how many visual spaces we have until the next tabstop.
+ $tab_visual = ($visual_depth + $tab_width);
+ $tab_visual = $tab_visual - ($tab_visual % $tab_width);
+ $tab_visual = ($tab_visual - $visual_depth);
+
+ // If this tab would take us over the limit, we're all done.
+ $remaining_depth = ($depth - $visual_depth);
+ if ($remaining_depth < $tab_visual) {
+ break;
+ }
+
+ $character_depth++;
+ $visual_depth += $tab_visual;
+ continue;
+ }
+
+ break;
+ }
+
+ return $character_depth;
+ }
+
+ private function updateChangeTypesForNormalization() {
+ if (!$this->getNormalized()) {
+ return;
+ }
+
+ // If we've parsed based on a normalized diff alignment, we may currently
+ // believe some lines are unchanged when they have actually changed. This
+ // happens when:
+ //
+ // - a line changes;
+ // - the change is a kind of change we normalize away when aligning the
+ // diff, like an indentation change;
+ // - we normalize the change away to align the diff; and so
+ // - the old and new copies of the line are now aligned in the new
+ // normalized diff.
+ //
+ // Then we end up with an alignment where the two lines that differ only
+ // in some some trivial way are aligned. This is great, and exactly what
+ // we're trying to accomplish by doing all this alignment stuff in the
+ // first place.
+ //
+ // However, in this case the correctly-aligned lines will be incorrectly
+ // marked as unchanged because the diff alorithm was fed normalized copies
+ // of the lines, and these copies truly weren't any different.
+ //
+ // When lines are aligned and marked identical, but they're not actually
+ // identcal, we now mark them as changed. The rest of the processing will
+ // figure out how to render them appropritely.
+
+ $new = $this->getNewLines();
+ $old = $this->getOldLines();
+ foreach ($old as $key => $o) {
+ $n = $new[$key];
+
+ if (!$o || !$n) {
+ continue;
+ }
+
+ if ($o['type'] === null && $n['type'] === null) {
+ if ($o['text'] !== $n['text']) {
+ $old[$key]['type'] = '-';
+ $new[$key]['type'] = '+';
+ }
+ }
+ }
+
+ $this->setOldLines($old);
+ $this->setNewLines($new);
+ }
+
+
}
diff --git a/src/applications/differential/parser/DifferentialLineAdjustmentMap.php b/src/applications/differential/parser/DifferentialLineAdjustmentMap.php
index fde8f61f7..e30f2ca86 100644
--- a/src/applications/differential/parser/DifferentialLineAdjustmentMap.php
+++ b/src/applications/differential/parser/DifferentialLineAdjustmentMap.php
@@ -1,376 +1,375 @@
<?php
/**
* Datastructure which follows lines of code across source changes.
*
* This map is used to update the positions of inline comments after diff
* updates. For example, if a inline comment appeared on line 30 of a diff
* but the next update adds 15 more lines above it, the comment should move
* down to line 45.
*
*/
final class DifferentialLineAdjustmentMap extends Phobject {
private $map;
private $nearestMap;
private $isInverse;
private $finalOffset;
private $nextMapInChain;
/**
* Get the raw adjustment map.
*/
public function getMap() {
return $this->map;
}
public function getNearestMap() {
if ($this->nearestMap === null) {
$this->buildNearestMap();
}
return $this->nearestMap;
}
public function getFinalOffset() {
// Make sure we've built this map already.
$this->getNearestMap();
return $this->finalOffset;
}
/**
* Add a map to the end of the chain.
*
* When a line is mapped with @{method:mapLine}, it is mapped through all
* maps in the chain.
*/
public function addMapToChain(DifferentialLineAdjustmentMap $map) {
if ($this->nextMapInChain) {
$this->nextMapInChain->addMapToChain($map);
} else {
$this->nextMapInChain = $map;
}
return $this;
}
/**
* Map a line across a change, or a series of changes.
*
* @param int Line to map
* @param bool True to map it as the end of a range.
* @return wild Spooky magic.
*/
public function mapLine($line, $is_end) {
$nmap = $this->getNearestMap();
$deleted = false;
$offset = false;
if (isset($nmap[$line])) {
$line_range = $nmap[$line];
if ($is_end) {
$to_line = end($line_range);
} else {
$to_line = reset($line_range);
}
if ($to_line <= 0) {
// If we're tracing the first line and this block is collapsing,
// compute the offset from the top of the block.
if (!$is_end && $this->isInverse) {
$offset = 0;
$cursor = $line - 1;
while (isset($nmap[$cursor])) {
$prev = $nmap[$cursor];
$prev = reset($prev);
if ($prev == $to_line) {
$offset++;
} else {
break;
}
$cursor--;
}
}
$to_line = -$to_line;
if (!$this->isInverse) {
$deleted = true;
}
}
$line = $to_line;
} else {
$line = $line + $this->finalOffset;
}
if ($this->nextMapInChain) {
$chain = $this->nextMapInChain->mapLine($line, $is_end);
list($chain_deleted, $chain_offset, $line) = $chain;
$deleted = ($deleted || $chain_deleted);
if ($chain_offset !== false) {
if ($offset === false) {
$offset = 0;
}
$offset += $chain_offset;
}
}
return array($deleted, $offset, $line);
}
/**
* Build a derived map which maps deleted lines to the nearest valid line.
*
* This computes a "nearest line" map and a final-line offset. These
* derived maps allow us to map deleted code to the previous (or next) line
* which actually exists.
*/
private function buildNearestMap() {
$map = $this->map;
$nmap = array();
$nearest = 0;
foreach ($map as $key => $value) {
if ($value) {
$nmap[$key] = $value;
$nearest = end($value);
} else {
$nmap[$key][0] = -$nearest;
}
}
if (isset($key)) {
$this->finalOffset = ($nearest - $key);
} else {
$this->finalOffset = 0;
}
foreach (array_reverse($map, true) as $key => $value) {
if ($value) {
$nearest = reset($value);
} else {
$nmap[$key][1] = -$nearest;
}
}
$this->nearestMap = $nmap;
return $this;
}
public static function newFromHunks(array $hunks) {
assert_instances_of($hunks, 'DifferentialHunk');
$map = array();
$o = 0;
$n = 0;
$hunks = msort($hunks, 'getOldOffset');
foreach ($hunks as $hunk) {
// If the hunks are disjoint, add the implied missing lines where
// nothing changed.
$min = ($hunk->getOldOffset() - 1);
while ($o < $min) {
$o++;
$n++;
$map[$o][] = $n;
}
$lines = $hunk->getStructuredLines();
foreach ($lines as $line) {
switch ($line['type']) {
case '-':
$o++;
$map[$o] = array();
break;
case '+':
$n++;
$map[$o][] = $n;
break;
case ' ':
$o++;
$n++;
$map[$o][] = $n;
break;
default:
break;
}
}
}
$map = self::reduceMapRanges($map);
return self::newFromMap($map);
}
public static function newFromMap(array $map) {
$obj = new DifferentialLineAdjustmentMap();
$obj->map = $map;
return $obj;
}
public static function newInverseMap(DifferentialLineAdjustmentMap $map) {
$old = $map->getMap();
$inv = array();
$last = 0;
foreach ($old as $k => $v) {
if (count($v) > 1) {
$v = range(reset($v), end($v));
}
if ($k == 0) {
foreach ($v as $line) {
$inv[$line] = array();
$last = $line;
}
} else if ($v) {
$first = true;
foreach ($v as $line) {
if ($first) {
$first = false;
$inv[$line][] = $k;
$last = $line;
} else {
$inv[$line] = array();
}
}
} else {
$inv[$last][] = $k;
}
}
$inv = self::reduceMapRanges($inv);
$obj = new DifferentialLineAdjustmentMap();
$obj->map = $inv;
$obj->isInverse = !$map->isInverse;
return $obj;
}
private static function reduceMapRanges(array $map) {
foreach ($map as $key => $values) {
if (count($values) > 2) {
$map[$key] = array(reset($values), end($values));
}
}
return $map;
}
public static function loadMaps(array $maps) {
$keys = array();
foreach ($maps as $map) {
list($u, $v) = $map;
$keys[self::getCacheKey($u, $v)] = $map;
}
$cache = new PhabricatorKeyValueDatabaseCache();
$cache = new PhutilKeyValueCacheProfiler($cache);
$cache->setProfiler(PhutilServiceProfiler::getInstance());
$results = array();
if ($keys) {
$caches = $cache->getKeys(array_keys($keys));
foreach ($caches as $key => $value) {
list($u, $v) = $keys[$key];
try {
$results[$u][$v] = self::newFromMap(
phutil_json_decode($value));
} catch (Exception $ex) {
// Ignore, rebuild below.
}
unset($keys[$key]);
}
}
if ($keys) {
$built = self::buildMaps($maps);
$write = array();
foreach ($built as $u => $list) {
foreach ($list as $v => $map) {
$write[self::getCacheKey($u, $v)] = json_encode($map->getMap());
$results[$u][$v] = $map;
}
}
$cache->setKeys($write);
}
return $results;
}
private static function buildMaps(array $maps) {
$need = array();
foreach ($maps as $map) {
list($u, $v) = $map;
$need[$u] = $u;
$need[$v] = $v;
}
if ($need) {
$changesets = id(new DifferentialChangesetQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs($need)
->needHunks(true)
->execute();
$changesets = mpull($changesets, null, 'getID');
}
$results = array();
foreach ($maps as $map) {
list($u, $v) = $map;
$u_set = idx($changesets, $u);
$v_set = idx($changesets, $v);
if (!$u_set || !$v_set) {
continue;
}
// This is the simple case.
if ($u == $v) {
$results[$u][$v] = self::newFromHunks(
$u_set->getHunks());
continue;
}
$u_old = $u_set->makeOldFile();
$v_old = $v_set->makeOldFile();
// No difference between the two left sides.
if ($u_old == $v_old) {
$results[$u][$v] = self::newFromMap(
array());
continue;
}
// If we're missing context, this won't currently work. We can
// make this case work, but it's fairly rare.
$u_hunks = $u_set->getHunks();
$v_hunks = $v_set->getHunks();
if (count($u_hunks) != 1 ||
count($v_hunks) != 1 ||
head($u_hunks)->getOldOffset() != 1 ||
head($u_hunks)->getNewOffset() != 1 ||
head($v_hunks)->getOldOffset() != 1 ||
head($v_hunks)->getNewOffset() != 1) {
continue;
}
$changeset = id(new PhabricatorDifferenceEngine())
- ->setIgnoreWhitespace(true)
->generateChangesetFromFileContent($u_old, $v_old);
$results[$u][$v] = self::newFromHunks(
$changeset->getHunks());
}
return $results;
}
private static function getCacheKey($u, $v) {
return 'diffadjust.v1('.$u.','.$v.')';
}
}
diff --git a/src/applications/differential/query/DifferentialRevisionQuery.php b/src/applications/differential/query/DifferentialRevisionQuery.php
index fdd4904be..a385fc525 100644
--- a/src/applications/differential/query/DifferentialRevisionQuery.php
+++ b/src/applications/differential/query/DifferentialRevisionQuery.php
@@ -1,1029 +1,1028 @@
<?php
/**
* @task config Query Configuration
* @task exec Query Execution
* @task internal Internals
*/
final class DifferentialRevisionQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $pathIDs = array();
private $authors = array();
private $draftAuthors = array();
private $ccs = array();
private $reviewers = array();
private $revIDs = array();
private $commitHashes = array();
private $commitPHIDs = array();
private $phids = array();
private $responsibles = array();
private $branches = array();
private $repositoryPHIDs;
private $updatedEpochMin;
private $updatedEpochMax;
private $statuses;
private $isOpen;
private $createdEpochMin;
private $createdEpochMax;
const ORDER_MODIFIED = 'order-modified';
const ORDER_CREATED = 'order-created';
private $needActiveDiffs = false;
private $needDiffIDs = false;
private $needCommitPHIDs = false;
private $needHashes = false;
private $needReviewers = false;
private $needReviewerAuthority;
private $needDrafts;
private $needFlags;
/* -( Query Configuration )------------------------------------------------ */
/**
* Filter results to revisions which affect a Diffusion path ID in a given
* repository. You can call this multiple times to select revisions for
* several paths.
*
* @param int Diffusion repository ID.
* @param int Diffusion path ID.
* @return this
* @task config
*/
public function withPath($repository_id, $path_id) {
$this->pathIDs[] = array(
'repositoryID' => $repository_id,
'pathID' => $path_id,
);
return $this;
}
/**
* Filter results to revisions authored by one of the given PHIDs. Calling
* this function will clear anything set by previous calls to
* @{method:withAuthors}.
*
* @param array List of PHIDs of authors
* @return this
* @task config
*/
public function withAuthors(array $author_phids) {
$this->authors = $author_phids;
return $this;
}
/**
* Filter results to revisions which CC one of the listed people. Calling this
* function will clear anything set by previous calls to @{method:withCCs}.
*
* @param array List of PHIDs of subscribers.
* @return this
* @task config
*/
public function withCCs(array $cc_phids) {
$this->ccs = $cc_phids;
return $this;
}
/**
* Filter results to revisions that have one of the provided PHIDs as
* reviewers. Calling this function will clear anything set by previous calls
* to @{method:withReviewers}.
*
* @param array List of PHIDs of reviewers
* @return this
* @task config
*/
public function withReviewers(array $reviewer_phids) {
$this->reviewers = $reviewer_phids;
return $this;
}
/**
* Filter results to revisions that have one of the provided commit hashes.
* Calling this function will clear anything set by previous calls to
* @{method:withCommitHashes}.
*
* @param array List of pairs <Class
* ArcanistDifferentialRevisionHash::HASH_$type constant,
* hash>
* @return this
* @task config
*/
public function withCommitHashes(array $commit_hashes) {
$this->commitHashes = $commit_hashes;
return $this;
}
/**
* Filter results to revisions that have one of the provided PHIDs as
* commits. Calling this function will clear anything set by previous calls
* to @{method:withCommitPHIDs}.
*
* @param array List of PHIDs of commits
* @return this
* @task config
*/
public function withCommitPHIDs(array $commit_phids) {
$this->commitPHIDs = $commit_phids;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withIsOpen($is_open) {
$this->isOpen = $is_open;
return $this;
}
/**
* Filter results to revisions on given branches.
*
* @param list List of branch names.
* @return this
* @task config
*/
public function withBranches(array $branches) {
$this->branches = $branches;
return $this;
}
/**
* Filter results to only return revisions whose ids are in the given set.
*
* @param array List of revision ids
* @return this
* @task config
*/
public function withIDs(array $ids) {
$this->revIDs = $ids;
return $this;
}
/**
* Filter results to only return revisions whose PHIDs are in the given set.
*
* @param array List of revision PHIDs
* @return this
* @task config
*/
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
/**
* Given a set of users, filter results to return only revisions they are
* responsible for (i.e., they are either authors or reviewers).
*
* @param array List of user PHIDs.
* @return this
* @task config
*/
public function withResponsibleUsers(array $responsible_phids) {
$this->responsibles = $responsible_phids;
return $this;
}
public function withRepositoryPHIDs(array $repository_phids) {
$this->repositoryPHIDs = $repository_phids;
return $this;
}
public function withUpdatedEpochBetween($min, $max) {
$this->updatedEpochMin = $min;
$this->updatedEpochMax = $max;
return $this;
}
public function withCreatedEpochBetween($min, $max) {
$this->createdEpochMin = $min;
$this->createdEpochMax = $max;
return $this;
}
/**
* Set whether or not the query should load the active diff for each
* revision.
*
* @param bool True to load and attach diffs.
* @return this
* @task config
*/
public function needActiveDiffs($need_active_diffs) {
$this->needActiveDiffs = $need_active_diffs;
return $this;
}
/**
* Set whether or not the query should load the associated commit PHIDs for
* each revision.
*
* @param bool True to load and attach diffs.
* @return this
* @task config
*/
public function needCommitPHIDs($need_commit_phids) {
$this->needCommitPHIDs = $need_commit_phids;
return $this;
}
/**
* Set whether or not the query should load associated diff IDs for each
* revision.
*
* @param bool True to load and attach diff IDs.
* @return this
* @task config
*/
public function needDiffIDs($need_diff_ids) {
$this->needDiffIDs = $need_diff_ids;
return $this;
}
/**
* Set whether or not the query should load associated commit hashes for each
* revision.
*
* @param bool True to load and attach commit hashes.
* @return this
* @task config
*/
public function needHashes($need_hashes) {
$this->needHashes = $need_hashes;
return $this;
}
/**
* Set whether or not the query should load associated reviewers.
*
* @param bool True to load and attach reviewers.
* @return this
* @task config
*/
public function needReviewers($need_reviewers) {
$this->needReviewers = $need_reviewers;
return $this;
}
/**
* Request information about the viewer's authority to act on behalf of each
* reviewer. In particular, they have authority to act on behalf of projects
* they are a member of.
*
* @param bool True to load and attach authority.
* @return this
* @task config
*/
public function needReviewerAuthority($need_reviewer_authority) {
$this->needReviewerAuthority = $need_reviewer_authority;
return $this;
}
public function needFlags($need_flags) {
$this->needFlags = $need_flags;
return $this;
}
public function needDrafts($need_drafts) {
$this->needDrafts = $need_drafts;
return $this;
}
/* -( Query Execution )---------------------------------------------------- */
public function newResultObject() {
return new DifferentialRevision();
}
/**
* Execute the query as configured, returning matching
* @{class:DifferentialRevision} objects.
*
* @return list List of matching DifferentialRevision objects.
* @task exec
*/
protected function loadPage() {
$data = $this->loadData();
$data = $this->didLoadRawRows($data);
$table = $this->newResultObject();
return $table->loadAllFromArray($data);
}
protected function willFilterPage(array $revisions) {
$viewer = $this->getViewer();
$repository_phids = mpull($revisions, 'getRepositoryPHID');
$repository_phids = array_filter($repository_phids);
$repositories = array();
if ($repository_phids) {
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withPHIDs($repository_phids)
->execute();
$repositories = mpull($repositories, null, 'getPHID');
}
// If a revision is associated with a repository:
//
// - the viewer must be able to see the repository; or
// - the viewer must have an automatic view capability.
//
// In the latter case, we'll load the revision but not load the repository.
$can_view = PhabricatorPolicyCapability::CAN_VIEW;
foreach ($revisions as $key => $revision) {
$repo_phid = $revision->getRepositoryPHID();
if (!$repo_phid) {
// The revision has no associated repository. Attach `null` and move on.
$revision->attachRepository(null);
continue;
}
$repository = idx($repositories, $repo_phid);
if ($repository) {
// The revision has an associated repository, and the viewer can see
// it. Attach it and move on.
$revision->attachRepository($repository);
continue;
}
if ($revision->hasAutomaticCapability($can_view, $viewer)) {
// The revision has an associated repository which the viewer can not
// see, but the viewer has an automatic capability on this revision.
// Load the revision without attaching a repository.
$revision->attachRepository(null);
continue;
}
if ($this->getViewer()->isOmnipotent()) {
// The viewer is omnipotent. Allow the revision to load even without
// a repository.
$revision->attachRepository(null);
continue;
}
// The revision has an associated repository, and the viewer can't see
// it, and the viewer has no special capabilities. Filter out this
// revision.
$this->didRejectResult($revision);
unset($revisions[$key]);
}
if (!$revisions) {
return array();
}
$table = new DifferentialRevision();
$conn_r = $table->establishConnection('r');
if ($this->needCommitPHIDs) {
$this->loadCommitPHIDs($conn_r, $revisions);
}
$need_active = $this->needActiveDiffs;
$need_ids = $need_active || $this->needDiffIDs;
if ($need_ids) {
$this->loadDiffIDs($conn_r, $revisions);
}
if ($need_active) {
$this->loadActiveDiffs($conn_r, $revisions);
}
if ($this->needHashes) {
$this->loadHashes($conn_r, $revisions);
}
if ($this->needReviewers || $this->needReviewerAuthority) {
$this->loadReviewers($conn_r, $revisions);
}
return $revisions;
}
protected function didFilterPage(array $revisions) {
$viewer = $this->getViewer();
if ($this->needFlags) {
$flags = id(new PhabricatorFlagQuery())
->setViewer($viewer)
->withOwnerPHIDs(array($viewer->getPHID()))
->withObjectPHIDs(mpull($revisions, 'getPHID'))
->execute();
$flags = mpull($flags, null, 'getObjectPHID');
foreach ($revisions as $revision) {
$revision->attachFlag(
$viewer,
idx($flags, $revision->getPHID()));
}
}
if ($this->needDrafts) {
PhabricatorDraftEngine::attachDrafts(
$viewer,
$revisions);
}
return $revisions;
}
private function loadData() {
$table = $this->newResultObject();
$conn = $table->establishConnection('r');
$selects = array();
// NOTE: If the query includes "responsiblePHIDs", we execute it as a
// UNION of revisions they own and revisions they're reviewing. This has
// much better performance than doing it with JOIN/WHERE.
if ($this->responsibles) {
$basic_authors = $this->authors;
$basic_reviewers = $this->reviewers;
try {
// Build the query where the responsible users are authors.
$this->authors = array_merge($basic_authors, $this->responsibles);
$this->reviewers = $basic_reviewers;
$selects[] = $this->buildSelectStatement($conn);
// Build the query where the responsible users are reviewers, or
// projects they are members of are reviewers.
$this->authors = $basic_authors;
$this->reviewers = array_merge($basic_reviewers, $this->responsibles);
$selects[] = $this->buildSelectStatement($conn);
// Put everything back like it was.
$this->authors = $basic_authors;
$this->reviewers = $basic_reviewers;
} catch (Exception $ex) {
$this->authors = $basic_authors;
$this->reviewers = $basic_reviewers;
throw $ex;
}
} else {
$selects[] = $this->buildSelectStatement($conn);
}
if (count($selects) > 1) {
$unions = null;
foreach ($selects as $select) {
if (!$unions) {
$unions = $select;
continue;
}
$unions = qsprintf(
$conn,
'%Q UNION DISTINCT %Q',
$unions,
$select);
}
$query = qsprintf(
$conn,
'%Q %Q %Q',
$unions,
$this->buildOrderClause($conn, true),
$this->buildLimitClause($conn));
} else {
$query = head($selects);
}
return queryfx_all($conn, '%Q', $query);
}
private function buildSelectStatement(AphrontDatabaseConnection $conn_r) {
$table = new DifferentialRevision();
$select = $this->buildSelectClause($conn_r);
$from = qsprintf(
$conn_r,
'FROM %T r',
$table->getTableName());
$joins = $this->buildJoinsClause($conn_r);
$where = $this->buildWhereClause($conn_r);
$group_by = $this->buildGroupClause($conn_r);
$having = $this->buildHavingClause($conn_r);
$order_by = $this->buildOrderClause($conn_r);
$limit = $this->buildLimitClause($conn_r);
return qsprintf(
$conn_r,
'(%Q %Q %Q %Q %Q %Q %Q %Q)',
$select,
$from,
$joins,
$where,
$group_by,
$having,
$order_by,
$limit);
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
private function buildJoinsClause(AphrontDatabaseConnection $conn) {
$joins = array();
if ($this->pathIDs) {
$path_table = new DifferentialAffectedPath();
$joins[] = qsprintf(
$conn,
'JOIN %T p ON p.revisionID = r.id',
$path_table->getTableName());
}
if ($this->commitHashes) {
$joins[] = qsprintf(
$conn,
'JOIN %T hash_rel ON hash_rel.revisionID = r.id',
ArcanistDifferentialRevisionHash::TABLE_NAME);
}
if ($this->ccs) {
$joins[] = qsprintf(
$conn,
'JOIN %T e_ccs ON e_ccs.src = r.phid '.
'AND e_ccs.type = %s '.
'AND e_ccs.dst in (%Ls)',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorObjectHasSubscriberEdgeType::EDGECONST,
$this->ccs);
}
if ($this->reviewers) {
$joins[] = qsprintf(
$conn,
'JOIN %T reviewer ON reviewer.revisionPHID = r.phid
AND reviewer.reviewerStatus != %s
AND reviewer.reviewerPHID in (%Ls)',
id(new DifferentialReviewer())->getTableName(),
DifferentialReviewerStatus::STATUS_RESIGNED,
$this->reviewers);
}
if ($this->draftAuthors) {
$joins[] = qsprintf(
$conn,
'JOIN %T has_draft ON has_draft.srcPHID = r.phid
AND has_draft.type = %s
AND has_draft.dstPHID IN (%Ls)',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorObjectHasDraftEdgeType::EDGECONST,
$this->draftAuthors);
}
if ($this->commitPHIDs) {
$joins[] = qsprintf(
$conn,
'JOIN %T commits ON commits.revisionID = r.id',
DifferentialRevision::TABLE_COMMIT);
}
$joins[] = $this->buildJoinClauseParts($conn);
return $this->formatJoinClause($conn, $joins);
}
/**
* @task internal
*/
protected function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
if ($this->pathIDs) {
$path_clauses = array();
$repo_info = igroup($this->pathIDs, 'repositoryID');
foreach ($repo_info as $repository_id => $paths) {
$path_clauses[] = qsprintf(
$conn,
'(p.repositoryID = %d AND p.pathID IN (%Ld))',
$repository_id,
ipull($paths, 'pathID'));
}
$path_clauses = qsprintf($conn, '%LO', $path_clauses);
$where[] = $path_clauses;
}
if ($this->authors) {
$where[] = qsprintf(
$conn,
'r.authorPHID IN (%Ls)',
$this->authors);
}
if ($this->revIDs) {
$where[] = qsprintf(
$conn,
'r.id IN (%Ld)',
$this->revIDs);
}
if ($this->repositoryPHIDs) {
$where[] = qsprintf(
$conn,
'r.repositoryPHID IN (%Ls)',
$this->repositoryPHIDs);
}
if ($this->commitHashes) {
$hash_clauses = array();
foreach ($this->commitHashes as $info) {
list($type, $hash) = $info;
$hash_clauses[] = qsprintf(
$conn,
'(hash_rel.type = %s AND hash_rel.hash = %s)',
$type,
$hash);
}
$hash_clauses = qsprintf($conn, '%LO', $hash_clauses);
$where[] = $hash_clauses;
}
if ($this->commitPHIDs) {
$where[] = qsprintf(
$conn,
'commits.commitPHID IN (%Ls)',
$this->commitPHIDs);
}
if ($this->phids) {
$where[] = qsprintf(
$conn,
'r.phid IN (%Ls)',
$this->phids);
}
if ($this->branches) {
$where[] = qsprintf(
$conn,
'r.branchName in (%Ls)',
$this->branches);
}
if ($this->updatedEpochMin !== null) {
$where[] = qsprintf(
$conn,
'r.dateModified >= %d',
$this->updatedEpochMin);
}
if ($this->updatedEpochMax !== null) {
$where[] = qsprintf(
$conn,
'r.dateModified <= %d',
$this->updatedEpochMax);
}
if ($this->createdEpochMin !== null) {
$where[] = qsprintf(
$conn,
'r.dateCreated >= %d',
$this->createdEpochMin);
}
if ($this->createdEpochMax !== null) {
$where[] = qsprintf(
$conn,
'r.dateCreated <= %d',
$this->createdEpochMax);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'r.status in (%Ls)',
$this->statuses);
}
if ($this->isOpen !== null) {
if ($this->isOpen) {
$statuses = DifferentialLegacyQuery::getModernValues(
DifferentialLegacyQuery::STATUS_OPEN);
} else {
$statuses = DifferentialLegacyQuery::getModernValues(
DifferentialLegacyQuery::STATUS_CLOSED);
}
$where[] = qsprintf(
$conn,
'r.status in (%Ls)',
$statuses);
}
$where[] = $this->buildWhereClauseParts($conn);
return $this->formatWhereClause($conn, $where);
}
/**
* @task internal
*/
protected function shouldGroupQueryResultRows() {
$join_triggers = array_merge(
$this->pathIDs,
$this->ccs,
$this->reviewers);
if (count($join_triggers) > 1) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
public function getBuiltinOrders() {
$orders = parent::getBuiltinOrders() + array(
'updated' => array(
'vector' => array('updated', 'id'),
'name' => pht('Date Updated (Latest First)'),
'aliases' => array(self::ORDER_MODIFIED),
),
'outdated' => array(
'vector' => array('-updated', '-id'),
'name' => pht('Date Updated (Oldest First)'),
),
);
// Alias the "newest" builtin to the historical key for it.
$orders['newest']['aliases'][] = self::ORDER_CREATED;
return $orders;
}
protected function getDefaultOrderVector() {
return array('updated', 'id');
}
public function getOrderableColumns() {
return array(
'updated' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'dateModified',
'type' => 'int',
),
) + parent::getOrderableColumns();
}
- protected function getPagingValueMap($cursor, array $keys) {
- $revision = $this->loadCursorObject($cursor);
+ protected function newPagingMapFromPartialObject($object) {
return array(
- 'id' => $revision->getID(),
- 'updated' => $revision->getDateModified(),
+ 'id' => (int)$object->getID(),
+ 'updated' => (int)$object->getDateModified(),
);
}
private function loadCommitPHIDs($conn_r, array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$commit_phids = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE revisionID IN (%Ld)',
DifferentialRevision::TABLE_COMMIT,
mpull($revisions, 'getID'));
$commit_phids = igroup($commit_phids, 'revisionID');
foreach ($revisions as $revision) {
$phids = idx($commit_phids, $revision->getID(), array());
$phids = ipull($phids, 'commitPHID');
$revision->attachCommitPHIDs($phids);
}
}
private function loadDiffIDs($conn_r, array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$diff_table = new DifferentialDiff();
$diff_ids = queryfx_all(
$conn_r,
'SELECT revisionID, id FROM %T WHERE revisionID IN (%Ld)
ORDER BY id DESC',
$diff_table->getTableName(),
mpull($revisions, 'getID'));
$diff_ids = igroup($diff_ids, 'revisionID');
foreach ($revisions as $revision) {
$ids = idx($diff_ids, $revision->getID(), array());
$ids = ipull($ids, 'id');
$revision->attachDiffIDs($ids);
}
}
private function loadActiveDiffs($conn_r, array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$diff_table = new DifferentialDiff();
$load_ids = array();
foreach ($revisions as $revision) {
$diffs = $revision->getDiffIDs();
if ($diffs) {
$load_ids[] = max($diffs);
}
}
$active_diffs = array();
if ($load_ids) {
$active_diffs = $diff_table->loadAllWhere(
'id IN (%Ld)',
$load_ids);
}
$active_diffs = mpull($active_diffs, null, 'getRevisionID');
foreach ($revisions as $revision) {
$revision->attachActiveDiff(idx($active_diffs, $revision->getID()));
}
}
private function loadHashes(
AphrontDatabaseConnection $conn_r,
array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE revisionID IN (%Ld)',
'differential_revisionhash',
mpull($revisions, 'getID'));
$data = igroup($data, 'revisionID');
foreach ($revisions as $revision) {
$hashes = idx($data, $revision->getID(), array());
$list = array();
foreach ($hashes as $hash) {
$list[] = array($hash['type'], $hash['hash']);
}
$revision->attachHashes($list);
}
}
private function loadReviewers(
AphrontDatabaseConnection $conn,
array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$reviewer_table = new DifferentialReviewer();
$reviewer_rows = queryfx_all(
$conn,
'SELECT * FROM %T WHERE revisionPHID IN (%Ls)
ORDER BY id ASC',
$reviewer_table->getTableName(),
mpull($revisions, 'getPHID'));
$reviewer_list = $reviewer_table->loadAllFromArray($reviewer_rows);
$reviewer_map = mgroup($reviewer_list, 'getRevisionPHID');
foreach ($reviewer_map as $key => $reviewers) {
$reviewer_map[$key] = mpull($reviewers, null, 'getReviewerPHID');
}
$viewer = $this->getViewer();
$viewer_phid = $viewer->getPHID();
$allow_key = 'differential.allow-self-accept';
$allow_self = PhabricatorEnv::getEnvConfig($allow_key);
// Figure out which of these reviewers the viewer has authority to act as.
if ($this->needReviewerAuthority && $viewer_phid) {
$authority = $this->loadReviewerAuthority(
$revisions,
$reviewer_map,
$allow_self);
}
foreach ($revisions as $revision) {
$reviewers = idx($reviewer_map, $revision->getPHID(), array());
foreach ($reviewers as $reviewer_phid => $reviewer) {
if ($this->needReviewerAuthority) {
if (!$viewer_phid) {
// Logged-out users never have authority.
$has_authority = false;
} else if ((!$allow_self) &&
($revision->getAuthorPHID() == $viewer_phid)) {
// The author can never have authority unless we allow self-accept.
$has_authority = false;
} else {
// Otherwise, look up whether the viewer has authority.
$has_authority = isset($authority[$reviewer_phid]);
}
$reviewer->attachAuthority($viewer, $has_authority);
}
$reviewers[$reviewer_phid] = $reviewer;
}
$revision->attachReviewers($reviewers);
}
}
private function loadReviewerAuthority(
array $revisions,
array $reviewers,
$allow_self) {
$revision_map = mpull($revisions, null, 'getPHID');
$viewer_phid = $this->getViewer()->getPHID();
// Find all the project/package reviewers which the user may have authority
// over.
$project_phids = array();
$package_phids = array();
$project_type = PhabricatorProjectProjectPHIDType::TYPECONST;
$package_type = PhabricatorOwnersPackagePHIDType::TYPECONST;
foreach ($reviewers as $revision_phid => $reviewer_list) {
if (!$allow_self) {
if ($revision_map[$revision_phid]->getAuthorPHID() == $viewer_phid) {
// If self-review isn't permitted, the user will never have
// authority over projects on revisions they authored because you
// can't accept your own revisions, so we don't need to load any
// data about these reviewers.
continue;
}
}
foreach ($reviewer_list as $reviewer_phid => $reviewer) {
$phid_type = phid_get_type($reviewer_phid);
if ($phid_type == $project_type) {
$project_phids[] = $reviewer_phid;
}
if ($phid_type == $package_type) {
$package_phids[] = $reviewer_phid;
}
}
}
// The viewer has authority over themselves.
$user_authority = array_fuse(array($viewer_phid));
// And over any projects they are a member of.
$project_authority = array();
if ($project_phids) {
$project_authority = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withPHIDs($project_phids)
->withMemberPHIDs(array($viewer_phid))
->execute();
$project_authority = mpull($project_authority, 'getPHID');
$project_authority = array_fuse($project_authority);
}
// And over any packages they own.
$package_authority = array();
if ($package_phids) {
$package_authority = id(new PhabricatorOwnersPackageQuery())
->setViewer($this->getViewer())
->withPHIDs($package_phids)
->withAuthorityPHIDs(array($viewer_phid))
->execute();
$package_authority = mpull($package_authority, 'getPHID');
$package_authority = array_fuse($package_authority);
}
return $user_authority + $project_authority + $package_authority;
}
public function getQueryApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
protected function getPrimaryTableAlias() {
return 'r';
}
}
diff --git a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php
index 30acc71c8..df59db922 100644
--- a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php
+++ b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php
@@ -1,619 +1,624 @@
<?php
abstract class DifferentialChangesetHTMLRenderer
extends DifferentialChangesetRenderer {
public static function getHTMLRendererByKey($key) {
switch ($key) {
case '1up':
return new DifferentialChangesetOneUpRenderer();
case '2up':
default:
return new DifferentialChangesetTwoUpRenderer();
}
throw new Exception(pht('Unknown HTML renderer "%s"!', $key));
}
abstract protected function getRendererTableClass();
abstract public function getRowScaffoldForInline(
PHUIDiffInlineCommentView $view);
protected function renderChangeTypeHeader($force) {
$changeset = $this->getChangeset();
$change = $changeset->getChangeType();
$file = $changeset->getFileType();
$messages = array();
switch ($change) {
case DifferentialChangeType::TYPE_ADD:
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$messages[] = pht('This file was added.');
break;
case DifferentialChangeType::FILE_IMAGE:
$messages[] = pht('This image was added.');
break;
case DifferentialChangeType::FILE_DIRECTORY:
$messages[] = pht('This directory was added.');
break;
case DifferentialChangeType::FILE_BINARY:
$messages[] = pht('This binary file was added.');
break;
case DifferentialChangeType::FILE_SYMLINK:
$messages[] = pht('This symlink was added.');
break;
case DifferentialChangeType::FILE_SUBMODULE:
$messages[] = pht('This submodule was added.');
break;
}
break;
case DifferentialChangeType::TYPE_DELETE:
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$messages[] = pht('This file was deleted.');
break;
case DifferentialChangeType::FILE_IMAGE:
$messages[] = pht('This image was deleted.');
break;
case DifferentialChangeType::FILE_DIRECTORY:
$messages[] = pht('This directory was deleted.');
break;
case DifferentialChangeType::FILE_BINARY:
$messages[] = pht('This binary file was deleted.');
break;
case DifferentialChangeType::FILE_SYMLINK:
$messages[] = pht('This symlink was deleted.');
break;
case DifferentialChangeType::FILE_SUBMODULE:
$messages[] = pht('This submodule was deleted.');
break;
}
break;
case DifferentialChangeType::TYPE_MOVE_HERE:
$from = phutil_tag('strong', array(), $changeset->getOldFile());
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$messages[] = pht('This file was moved from %s.', $from);
break;
case DifferentialChangeType::FILE_IMAGE:
$messages[] = pht('This image was moved from %s.', $from);
break;
case DifferentialChangeType::FILE_DIRECTORY:
$messages[] = pht('This directory was moved from %s.', $from);
break;
case DifferentialChangeType::FILE_BINARY:
$messages[] = pht('This binary file was moved from %s.', $from);
break;
case DifferentialChangeType::FILE_SYMLINK:
$messages[] = pht('This symlink was moved from %s.', $from);
break;
case DifferentialChangeType::FILE_SUBMODULE:
$messages[] = pht('This submodule was moved from %s.', $from);
break;
}
break;
case DifferentialChangeType::TYPE_COPY_HERE:
$from = phutil_tag('strong', array(), $changeset->getOldFile());
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$messages[] = pht('This file was copied from %s.', $from);
break;
case DifferentialChangeType::FILE_IMAGE:
$messages[] = pht('This image was copied from %s.', $from);
break;
case DifferentialChangeType::FILE_DIRECTORY:
$messages[] = pht('This directory was copied from %s.', $from);
break;
case DifferentialChangeType::FILE_BINARY:
$messages[] = pht('This binary file was copied from %s.', $from);
break;
case DifferentialChangeType::FILE_SYMLINK:
$messages[] = pht('This symlink was copied from %s.', $from);
break;
case DifferentialChangeType::FILE_SUBMODULE:
$messages[] = pht('This submodule was copied from %s.', $from);
break;
}
break;
case DifferentialChangeType::TYPE_MOVE_AWAY:
$paths = phutil_tag(
'strong',
array(),
implode(', ', $changeset->getAwayPaths()));
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$messages[] = pht('This file was moved to %s.', $paths);
break;
case DifferentialChangeType::FILE_IMAGE:
$messages[] = pht('This image was moved to %s.', $paths);
break;
case DifferentialChangeType::FILE_DIRECTORY:
$messages[] = pht('This directory was moved to %s.', $paths);
break;
case DifferentialChangeType::FILE_BINARY:
$messages[] = pht('This binary file was moved to %s.', $paths);
break;
case DifferentialChangeType::FILE_SYMLINK:
$messages[] = pht('This symlink was moved to %s.', $paths);
break;
case DifferentialChangeType::FILE_SUBMODULE:
$messages[] = pht('This submodule was moved to %s.', $paths);
break;
}
break;
case DifferentialChangeType::TYPE_COPY_AWAY:
$paths = phutil_tag(
'strong',
array(),
implode(', ', $changeset->getAwayPaths()));
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$messages[] = pht('This file was copied to %s.', $paths);
break;
case DifferentialChangeType::FILE_IMAGE:
$messages[] = pht('This image was copied to %s.', $paths);
break;
case DifferentialChangeType::FILE_DIRECTORY:
$messages[] = pht('This directory was copied to %s.', $paths);
break;
case DifferentialChangeType::FILE_BINARY:
$messages[] = pht('This binary file was copied to %s.', $paths);
break;
case DifferentialChangeType::FILE_SYMLINK:
$messages[] = pht('This symlink was copied to %s.', $paths);
break;
case DifferentialChangeType::FILE_SUBMODULE:
$messages[] = pht('This submodule was copied to %s.', $paths);
break;
}
break;
case DifferentialChangeType::TYPE_MULTICOPY:
$paths = phutil_tag(
'strong',
array(),
implode(', ', $changeset->getAwayPaths()));
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$messages[] = pht(
'This file was deleted after being copied to %s.',
$paths);
break;
case DifferentialChangeType::FILE_IMAGE:
$messages[] = pht(
'This image was deleted after being copied to %s.',
$paths);
break;
case DifferentialChangeType::FILE_DIRECTORY:
$messages[] = pht(
'This directory was deleted after being copied to %s.',
$paths);
break;
case DifferentialChangeType::FILE_BINARY:
$messages[] = pht(
'This binary file was deleted after being copied to %s.',
$paths);
break;
case DifferentialChangeType::FILE_SYMLINK:
$messages[] = pht(
'This symlink was deleted after being copied to %s.',
$paths);
break;
case DifferentialChangeType::FILE_SUBMODULE:
$messages[] = pht(
'This submodule was deleted after being copied to %s.',
$paths);
break;
}
break;
default:
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
// This is the default case, so we only render this header if
// forced to since it's not very useful.
if ($force) {
$messages[] = pht('This file was not modified.');
}
break;
case DifferentialChangeType::FILE_IMAGE:
$messages[] = pht('This is an image.');
break;
case DifferentialChangeType::FILE_DIRECTORY:
$messages[] = pht('This is a directory.');
break;
case DifferentialChangeType::FILE_BINARY:
$messages[] = pht('This is a binary file.');
break;
case DifferentialChangeType::FILE_SYMLINK:
$messages[] = pht('This is a symlink.');
break;
case DifferentialChangeType::FILE_SUBMODULE:
$messages[] = pht('This is a submodule.');
break;
}
break;
}
return $this->formatHeaderMessages($messages);
}
protected function renderUndershieldHeader() {
$messages = array();
$changeset = $this->getChangeset();
$file = $changeset->getFileType();
// If this is a text file with at least one hunk, we may have converted
// the text encoding. In this case, show a note.
$show_encoding = ($file == DifferentialChangeType::FILE_TEXT) &&
($changeset->getHunks());
if ($show_encoding) {
$encoding = $this->getOriginalCharacterEncoding();
if ($encoding != 'utf8') {
if ($encoding) {
$messages[] = pht(
'This file was converted from %s for display.',
phutil_tag('strong', array(), $encoding));
} else {
$messages[] = pht('This file uses an unknown character encoding.');
}
}
}
if ($this->getHighlightingDisabled()) {
$messages[] = pht(
'This file is larger than %s, so syntax highlighting is '.
'disabled by default.',
phutil_format_bytes(DifferentialChangesetParser::HIGHLIGHT_BYTE_LIMIT));
}
return $this->formatHeaderMessages($messages);
}
private function formatHeaderMessages(array $messages) {
if (!$messages) {
return null;
}
foreach ($messages as $key => $message) {
$messages[$key] = phutil_tag('li', array(), $message);
}
return phutil_tag(
'ul',
array(
'class' => 'differential-meta-notice',
),
$messages);
}
protected function renderPropertyChangeHeader() {
$changeset = $this->getChangeset();
list($old, $new) = $this->getChangesetProperties($changeset);
// If we don't have any property changes, don't render this table.
if ($old === $new) {
return null;
}
$keys = array_keys($old + $new);
sort($keys);
$key_map = array(
'unix:filemode' => pht('File Mode'),
'file:dimensions' => pht('Image Dimensions'),
'file:mimetype' => pht('MIME Type'),
'file:size' => pht('File Size'),
);
$rows = array();
foreach ($keys as $key) {
$oval = idx($old, $key);
$nval = idx($new, $key);
if ($oval !== $nval) {
if ($oval === null) {
$oval = phutil_tag('em', array(), 'null');
} else {
$oval = phutil_escape_html_newlines($oval);
}
if ($nval === null) {
$nval = phutil_tag('em', array(), 'null');
} else {
$nval = phutil_escape_html_newlines($nval);
}
$readable_key = idx($key_map, $key, $key);
$row = array(
$readable_key,
$oval,
$nval,
);
$rows[] = $row;
}
}
$classes = array('', 'oval', 'nval');
$headers = array(
pht('Property'),
pht('Old Value'),
pht('New Value'),
);
$table = id(new AphrontTableView($rows))
->setHeaders($headers)
->setColumnClasses($classes);
return phutil_tag(
'div',
array(
'class' => 'differential-property-table',
),
$table);
}
public function renderShield($message, $force = 'default') {
$end = count($this->getOldLines());
$reference = $this->getRenderingReference();
if ($force !== 'text' &&
- $force !== 'whitespace' &&
$force !== 'none' &&
$force !== 'default') {
throw new Exception(
pht(
"Invalid '%s' parameter '%s'!",
'force',
$force));
}
$range = "0-{$end}";
if ($force == 'text') {
// If we're forcing text, force the whole file to be rendered.
$range = "{$range}/0-{$end}";
}
$meta = array(
'ref' => $reference,
'range' => $range,
);
- if ($force == 'whitespace') {
- $meta['whitespace'] = DifferentialChangesetParser::WHITESPACE_SHOW_ALL;
- }
-
$content = array();
$content[] = $message;
if ($force !== 'none') {
$content[] = ' ';
$content[] = javelin_tag(
'a',
array(
'mustcapture' => true,
'sigil' => 'show-more',
'class' => 'complete',
'href' => '#',
'meta' => $meta,
),
pht('Show File Contents'));
}
return $this->wrapChangeInTable(
javelin_tag(
'tr',
array(
'sigil' => 'context-target',
),
phutil_tag(
'td',
array(
'class' => 'differential-shield',
'colspan' => 6,
),
$content)));
}
abstract protected function renderColgroup();
protected function wrapChangeInTable($content) {
if (!$content) {
return null;
}
$classes = array();
$classes[] = 'differential-diff';
$classes[] = 'remarkup-code';
$classes[] = 'PhabricatorMonospaced';
$classes[] = $this->getRendererTableClass();
+ $sigils = array();
+ $sigils[] = 'differential-diff';
+ foreach ($this->getTableSigils() as $sigil) {
+ $sigils[] = $sigil;
+ }
+
return javelin_tag(
'table',
array(
'class' => implode(' ', $classes),
- 'sigil' => 'differential-diff',
+ 'sigil' => implode(' ', $sigils),
),
array(
$this->renderColgroup(),
$content,
));
}
+ protected function getTableSigils() {
+ return array();
+ }
+
protected function buildInlineComment(
PhabricatorInlineCommentInterface $comment,
$on_right = false) {
$user = $this->getUser();
$edit = $user &&
($comment->getAuthorPHID() == $user->getPHID()) &&
($comment->isDraft())
&& $this->getShowEditAndReplyLinks();
$allow_reply = (bool)$user && $this->getShowEditAndReplyLinks();
$allow_done = !$comment->isDraft() && $this->getCanMarkDone();
return id(new PHUIDiffInlineCommentDetailView())
->setUser($user)
->setInlineComment($comment)
->setIsOnRight($on_right)
->setHandles($this->getHandles())
->setMarkupEngine($this->getMarkupEngine())
->setEditable($edit)
->setAllowReply($allow_reply)
->setCanMarkDone($allow_done)
->setObjectOwnerPHID($this->getObjectOwnerPHID());
}
/**
* Build links which users can click to show more context in a changeset.
*
* @param int Beginning of the line range to build links for.
* @param int Length of the line range to build links for.
* @param int Total number of lines in the changeset.
* @return markup Rendered links.
*/
protected function renderShowContextLinks($top, $len, $changeset_length) {
$block_size = 20;
$end = ($top + $len) - $block_size;
// If this is a large block, such that the "top" and "bottom" ranges are
// non-overlapping, we'll provide options to show the top, bottom or entire
// block. For smaller blocks, we only provide an option to show the entire
// block, since it would be silly to show the bottom 20 lines of a 25-line
// block.
$is_large_block = ($len > ($block_size * 2));
$links = array();
if ($is_large_block) {
$is_first_block = ($top == 0);
if ($is_first_block) {
$text = pht('Show First %d Line(s)', $block_size);
} else {
$text = pht("\xE2\x96\xB2 Show %d Line(s)", $block_size);
}
$links[] = $this->renderShowContextLink(
false,
"{$top}-{$len}/{$top}-20",
$text);
}
$links[] = $this->renderShowContextLink(
true,
"{$top}-{$len}/{$top}-{$len}",
pht('Show All %d Line(s)', $len));
if ($is_large_block) {
$is_last_block = (($top + $len) >= $changeset_length);
if ($is_last_block) {
$text = pht('Show Last %d Line(s)', $block_size);
} else {
$text = "\xE2\x96\xBC ".pht('Show %d Line(s)', $block_size);
}
$links[] = $this->renderShowContextLink(
false,
"{$top}-{$len}/{$end}-20",
$text);
}
return phutil_implode_html(" \xE2\x80\xA2 ", $links);
}
/**
* Build a link that shows more context in a changeset.
*
* See @{method:renderShowContextLinks}.
*
* @param bool Does this link show all context when clicked?
* @param string Range specification for lines to show.
* @param string Text of the link.
* @return markup Rendered link.
*/
private function renderShowContextLink($is_all, $range, $text) {
$reference = $this->getRenderingReference();
return javelin_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'show-more',
'meta' => array(
'type' => ($is_all ? 'all' : null),
'range' => $range,
),
),
$text);
}
/**
* Build the prefixes for line IDs used to track inline comments.
*
* @return pair<wild, wild> Left and right prefixes.
*/
protected function getLineIDPrefixes() {
// These look like "C123NL45", which means the line is line 45 on the
// "new" side of the file in changeset 123.
// The "C" stands for "changeset", and is followed by a changeset ID.
// "N" stands for "new" and means the comment should attach to the new file
// when stored. "O" stands for "old" and means the comment should attach to
// the old file. These are important because either the old or new part
// of a file may appear on the left or right side of the diff in the
// diff-of-diffs view.
// The "L" stands for "line" and is followed by the line number.
if ($this->getOldChangesetID()) {
$left_prefix = array();
$left_prefix[] = 'C';
$left_prefix[] = $this->getOldChangesetID();
$left_prefix[] = $this->getOldAttachesToNewFile() ? 'N' : 'O';
$left_prefix[] = 'L';
$left_prefix = implode('', $left_prefix);
} else {
$left_prefix = null;
}
if ($this->getNewChangesetID()) {
$right_prefix = array();
$right_prefix[] = 'C';
$right_prefix[] = $this->getNewChangesetID();
$right_prefix[] = $this->getNewAttachesToNewFile() ? 'N' : 'O';
$right_prefix[] = 'L';
$right_prefix = implode('', $right_prefix);
} else {
$right_prefix = null;
}
return array($left_prefix, $right_prefix);
}
protected function renderImageStage(PhabricatorFile $file) {
return phutil_tag(
'div',
array(
'class' => 'differential-image-stage',
),
phutil_tag(
'img',
array(
'src' => $file->getBestURI(),
)));
}
}
diff --git a/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php b/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php
index 90c397790..289b80248 100644
--- a/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php
+++ b/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php
@@ -1,279 +1,293 @@
<?php
final class DifferentialChangesetOneUpRenderer
extends DifferentialChangesetHTMLRenderer {
public function isOneUpRenderer() {
return true;
}
protected function getRendererTableClass() {
return 'diff-1up';
}
public function getRendererKey() {
return '1up';
}
protected function renderColgroup() {
return phutil_tag('colgroup', array(), array(
phutil_tag('col', array('class' => 'num')),
phutil_tag('col', array('class' => 'num')),
phutil_tag('col', array('class' => 'copy')),
phutil_tag('col', array('class' => 'unified')),
));
}
public function renderTextChange(
$range_start,
$range_len,
$rows) {
$primitives = $this->buildPrimitives($range_start, $range_len);
return $this->renderPrimitives($primitives, $rows);
}
protected function renderPrimitives(array $primitives, $rows) {
list($left_prefix, $right_prefix) = $this->getLineIDPrefixes();
$no_copy = phutil_tag('td', array('class' => 'copy'));
$no_coverage = null;
$column_width = 4;
$aural_minus = javelin_tag(
'span',
array(
'aural' => true,
),
'- ');
$aural_plus = javelin_tag(
'span',
array(
'aural' => true,
),
'+ ');
$out = array();
foreach ($primitives as $k => $p) {
$type = $p['type'];
switch ($type) {
case 'old':
case 'new':
case 'old-file':
case 'new-file':
$is_old = ($type == 'old' || $type == 'old-file');
$cells = array();
if ($is_old) {
if ($p['htype']) {
if (empty($p['oline'])) {
$class = 'left old old-full';
} else {
$class = 'left old';
}
$aural = $aural_minus;
} else {
$class = 'left';
$aural = null;
}
if ($type == 'old-file') {
$class = "{$class} differential-old-image";
}
if ($left_prefix) {
$left_id = $left_prefix.$p['line'];
} else {
$left_id = null;
}
$line = $p['line'];
$cells[] = phutil_tag(
- 'th',
+ 'td',
array(
'id' => $left_id,
- 'class' => $class,
- ),
- $line);
+ 'class' => $class.' n',
+ 'data-n' => $line,
+ ));
$render = $p['render'];
if ($aural !== null) {
$render = array($aural, $render);
}
- $cells[] = phutil_tag('th', array('class' => $class));
+ $cells[] = phutil_tag(
+ 'td',
+ array(
+ 'class' => $class.' n',
+ ));
$cells[] = $no_copy;
$cells[] = phutil_tag('td', array('class' => $class), $render);
$cells[] = $no_coverage;
} else {
if ($p['htype']) {
if (empty($p['oline'])) {
$class = 'right new new-full';
} else {
$class = 'right new';
}
- $cells[] = phutil_tag('th', array('class' => $class));
+ $cells[] = phutil_tag(
+ 'td',
+ array(
+ 'class' => $class.' n',
+ ));
$aural = $aural_plus;
} else {
$class = 'right';
if ($left_prefix) {
$left_id = $left_prefix.$p['oline'];
} else {
$left_id = null;
}
$oline = $p['oline'];
- $cells[] = phutil_tag('th', array('id' => $left_id), $oline);
+ $cells[] = phutil_tag(
+ 'td',
+ array(
+ 'id' => $left_id,
+ 'class' => 'n',
+ 'data-n' => $oline,
+ ));
$aural = null;
}
if ($type == 'new-file') {
$class = "{$class} differential-new-image";
}
if ($right_prefix) {
$right_id = $right_prefix.$p['line'];
} else {
$right_id = null;
}
$line = $p['line'];
$cells[] = phutil_tag(
- 'th',
+ 'td',
array(
'id' => $right_id,
- 'class' => $class,
- ),
- $line);
+ 'class' => $class.' n',
+ 'data-n' => $line,
+ ));
$render = $p['render'];
if ($aural !== null) {
$render = array($aural, $render);
}
$cells[] = $no_copy;
$cells[] = phutil_tag('td', array('class' => $class), $render);
$cells[] = $no_coverage;
}
$out[] = phutil_tag('tr', array(), $cells);
break;
case 'inline':
$inline = $this->buildInlineComment(
$p['comment'],
$p['right']);
$out[] = $this->getRowScaffoldForInline($inline);
break;
case 'no-context':
$out[] = phutil_tag(
'tr',
array(),
phutil_tag(
'td',
array(
'class' => 'show-more',
'colspan' => $column_width,
),
pht('Context not available.')));
break;
case 'context':
$top = $p['top'];
$len = $p['len'];
$links = $this->renderShowContextLinks($top, $len, $rows);
$out[] = javelin_tag(
'tr',
array(
'sigil' => 'context-target',
),
phutil_tag(
'td',
array(
'class' => 'show-more',
'colspan' => $column_width,
),
$links));
break;
default:
$out[] = hsprintf('<tr><th /><th /><td>%s</td></tr>', $type);
break;
}
}
if ($out) {
return $this->wrapChangeInTable(phutil_implode_html('', $out));
}
return null;
}
public function renderFileChange(
$old_file = null,
$new_file = null,
$id = 0,
$vs = 0) {
// TODO: This should eventually merge into the normal primitives pathway,
// but fake it for now and just share as much code as possible.
$primitives = array();
if ($old_file) {
$primitives[] = array(
'type' => 'old-file',
'htype' => ($new_file ? 'new-file' : null),
'file' => $old_file,
'line' => 1,
'render' => $this->renderImageStage($old_file),
);
}
if ($new_file) {
$primitives[] = array(
'type' => 'new-file',
'htype' => ($old_file ? 'old-file' : null),
'file' => $new_file,
'line' => 1,
'oline' => ($old_file ? 1 : null),
'render' => $this->renderImageStage($new_file),
);
}
// TODO: We'd like to share primitive code here, but buildPrimitives()
// currently chokes on changesets with no textual data.
foreach ($this->getOldComments() as $line => $group) {
foreach ($group as $comment) {
$primitives[] = array(
'type' => 'inline',
'comment' => $comment,
'right' => false,
);
}
}
foreach ($this->getNewComments() as $line => $group) {
foreach ($group as $comment) {
$primitives[] = array(
'type' => 'inline',
'comment' => $comment,
'right' => true,
);
}
}
$output = $this->renderPrimitives($primitives, 1);
return $this->renderChangesetTable($output);
}
public function getRowScaffoldForInline(PHUIDiffInlineCommentView $view) {
return id(new PHUIDiffOneUpInlineCommentRowScaffold())
->addInlineView($view);
}
}
diff --git a/src/applications/differential/render/DifferentialChangesetRenderer.php b/src/applications/differential/render/DifferentialChangesetRenderer.php
index 450d160e2..26de5cb53 100644
--- a/src/applications/differential/render/DifferentialChangesetRenderer.php
+++ b/src/applications/differential/render/DifferentialChangesetRenderer.php
@@ -1,681 +1,718 @@
<?php
abstract class DifferentialChangesetRenderer extends Phobject {
private $user;
private $changeset;
private $renderingReference;
private $renderPropertyChangeHeader;
private $isTopLevel;
private $isUndershield;
private $hunkStartLines;
private $oldLines;
private $newLines;
private $oldComments;
private $newComments;
private $oldChangesetID;
private $newChangesetID;
private $oldAttachesToNewFile;
private $newAttachesToNewFile;
private $highlightOld = array();
private $highlightNew = array();
private $codeCoverage;
private $handles;
private $markupEngine;
private $oldRender;
private $newRender;
private $originalOld;
private $originalNew;
private $gaps;
private $mask;
- private $depths;
private $originalCharacterEncoding;
private $showEditAndReplyLinks;
private $canMarkDone;
private $objectOwnerPHID;
private $highlightingDisabled;
+ private $scopeEngine = false;
+ private $depthOnlyLines;
private $oldFile = false;
private $newFile = false;
abstract public function getRendererKey();
public function setShowEditAndReplyLinks($bool) {
$this->showEditAndReplyLinks = $bool;
return $this;
}
public function getShowEditAndReplyLinks() {
return $this->showEditAndReplyLinks;
}
public function setHighlightingDisabled($highlighting_disabled) {
$this->highlightingDisabled = $highlighting_disabled;
return $this;
}
public function getHighlightingDisabled() {
return $this->highlightingDisabled;
}
public function setOriginalCharacterEncoding($original_character_encoding) {
$this->originalCharacterEncoding = $original_character_encoding;
return $this;
}
public function getOriginalCharacterEncoding() {
return $this->originalCharacterEncoding;
}
public function setIsUndershield($is_undershield) {
$this->isUndershield = $is_undershield;
return $this;
}
public function getIsUndershield() {
return $this->isUndershield;
}
- public function setDepths($depths) {
- $this->depths = $depths;
- return $this;
- }
- protected function getDepths() {
- return $this->depths;
- }
-
public function setMask($mask) {
$this->mask = $mask;
return $this;
}
protected function getMask() {
return $this->mask;
}
public function setGaps($gaps) {
$this->gaps = $gaps;
return $this;
}
protected function getGaps() {
return $this->gaps;
}
+ public function setDepthOnlyLines(array $lines) {
+ $this->depthOnlyLines = $lines;
+ return $this;
+ }
+
+ public function getDepthOnlyLines() {
+ return $this->depthOnlyLines;
+ }
+
public function attachOldFile(PhabricatorFile $old = null) {
$this->oldFile = $old;
return $this;
}
public function getOldFile() {
if ($this->oldFile === false) {
throw new PhabricatorDataNotAttachedException($this);
}
return $this->oldFile;
}
public function hasOldFile() {
return (bool)$this->oldFile;
}
public function attachNewFile(PhabricatorFile $new = null) {
$this->newFile = $new;
return $this;
}
public function getNewFile() {
if ($this->newFile === false) {
throw new PhabricatorDataNotAttachedException($this);
}
return $this->newFile;
}
public function hasNewFile() {
return (bool)$this->newFile;
}
public function setOriginalNew($original_new) {
$this->originalNew = $original_new;
return $this;
}
protected function getOriginalNew() {
return $this->originalNew;
}
public function setOriginalOld($original_old) {
$this->originalOld = $original_old;
return $this;
}
protected function getOriginalOld() {
return $this->originalOld;
}
public function setNewRender($new_render) {
$this->newRender = $new_render;
return $this;
}
protected function getNewRender() {
return $this->newRender;
}
public function setOldRender($old_render) {
$this->oldRender = $old_render;
return $this;
}
protected function getOldRender() {
return $this->oldRender;
}
public function setMarkupEngine(PhabricatorMarkupEngine $markup_engine) {
$this->markupEngine = $markup_engine;
return $this;
}
public function getMarkupEngine() {
return $this->markupEngine;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
protected function getHandles() {
return $this->handles;
}
public function setCodeCoverage($code_coverage) {
$this->codeCoverage = $code_coverage;
return $this;
}
protected function getCodeCoverage() {
return $this->codeCoverage;
}
public function setHighlightNew($highlight_new) {
$this->highlightNew = $highlight_new;
return $this;
}
protected function getHighlightNew() {
return $this->highlightNew;
}
public function setHighlightOld($highlight_old) {
$this->highlightOld = $highlight_old;
return $this;
}
protected function getHighlightOld() {
return $this->highlightOld;
}
public function setNewAttachesToNewFile($attaches) {
$this->newAttachesToNewFile = $attaches;
return $this;
}
protected function getNewAttachesToNewFile() {
return $this->newAttachesToNewFile;
}
public function setOldAttachesToNewFile($attaches) {
$this->oldAttachesToNewFile = $attaches;
return $this;
}
protected function getOldAttachesToNewFile() {
return $this->oldAttachesToNewFile;
}
public function setNewChangesetID($new_changeset_id) {
$this->newChangesetID = $new_changeset_id;
return $this;
}
protected function getNewChangesetID() {
return $this->newChangesetID;
}
public function setOldChangesetID($old_changeset_id) {
$this->oldChangesetID = $old_changeset_id;
return $this;
}
protected function getOldChangesetID() {
return $this->oldChangesetID;
}
public function setNewComments(array $new_comments) {
foreach ($new_comments as $line_number => $comments) {
assert_instances_of($comments, 'PhabricatorInlineCommentInterface');
}
$this->newComments = $new_comments;
return $this;
}
protected function getNewComments() {
return $this->newComments;
}
public function setOldComments(array $old_comments) {
foreach ($old_comments as $line_number => $comments) {
assert_instances_of($comments, 'PhabricatorInlineCommentInterface');
}
$this->oldComments = $old_comments;
return $this;
}
protected function getOldComments() {
return $this->oldComments;
}
public function setNewLines(array $new_lines) {
$this->newLines = $new_lines;
return $this;
}
protected function getNewLines() {
return $this->newLines;
}
public function setOldLines(array $old_lines) {
$this->oldLines = $old_lines;
return $this;
}
protected function getOldLines() {
return $this->oldLines;
}
public function setHunkStartLines(array $hunk_start_lines) {
$this->hunkStartLines = $hunk_start_lines;
return $this;
}
protected function getHunkStartLines() {
return $this->hunkStartLines;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
protected function getUser() {
return $this->user;
}
public function setChangeset(DifferentialChangeset $changeset) {
$this->changeset = $changeset;
return $this;
}
protected function getChangeset() {
return $this->changeset;
}
public function setRenderingReference($rendering_reference) {
$this->renderingReference = $rendering_reference;
return $this;
}
protected function getRenderingReference() {
return $this->renderingReference;
}
public function setRenderPropertyChangeHeader($should_render) {
$this->renderPropertyChangeHeader = $should_render;
return $this;
}
private function shouldRenderPropertyChangeHeader() {
return $this->renderPropertyChangeHeader;
}
public function setIsTopLevel($is) {
$this->isTopLevel = $is;
return $this;
}
private function getIsTopLevel() {
return $this->isTopLevel;
}
public function setCanMarkDone($can_mark_done) {
$this->canMarkDone = $can_mark_done;
return $this;
}
public function getCanMarkDone() {
return $this->canMarkDone;
}
public function setObjectOwnerPHID($phid) {
$this->objectOwnerPHID = $phid;
return $this;
}
public function getObjectOwnerPHID() {
return $this->objectOwnerPHID;
}
final public function renderChangesetTable($content) {
$props = null;
if ($this->shouldRenderPropertyChangeHeader()) {
$props = $this->renderPropertyChangeHeader();
}
$notice = null;
if ($this->getIsTopLevel()) {
$force = (!$content && !$props);
$notice = $this->renderChangeTypeHeader($force);
}
$undershield = null;
if ($this->getIsUndershield()) {
$undershield = $this->renderUndershieldHeader();
}
- $result = $notice.$props.$undershield.$content;
-
- // TODO: Let the user customize their tab width / display style.
- // TODO: We should possibly post-process "\r" as well.
- // TODO: Both these steps should happen earlier.
- $result = str_replace("\t", ' ', $result);
+ $result = array(
+ $notice,
+ $props,
+ $undershield,
+ $content,
+ );
- return phutil_safe_html($result);
+ return hsprintf('%s', $result);
}
abstract public function isOneUpRenderer();
abstract public function renderTextChange(
$range_start,
$range_len,
$rows);
abstract public function renderFileChange(
$old = null,
$new = null,
$id = 0,
$vs = 0);
abstract protected function renderChangeTypeHeader($force);
abstract protected function renderUndershieldHeader();
protected function didRenderChangesetTableContents($contents) {
return $contents;
}
/**
* Render a "shield" over the diff, with a message like "This file is
* generated and does not need to be reviewed." or "This file was completely
* deleted." This UI element hides unimportant text so the reviewer doesn't
* need to scroll past it.
*
* The shield includes a link to view the underlying content. This link
* may force certain rendering modes when the link is clicked:
*
* - `"default"`: Render the diff normally, as though it was not
* shielded. This is the default and appropriate if the underlying
* diff is a normal change, but was hidden for reasons of not being
* important (e.g., generated code).
* - `"text"`: Force the text to be shown. This is probably only relevant
* when a file is not changed.
- * - `"whitespace"`: Force the text to be shown, and the diff to be
- * rendered with all whitespace shown. This is probably only relevant
- * when a file is changed only by altering whitespace.
* - `"none"`: Don't show the link (e.g., text not available).
*
* @param string Message explaining why the diff is hidden.
* @param string|null Force mode, see above.
* @return string Shield markup.
*/
abstract public function renderShield($message, $force = 'default');
abstract protected function renderPropertyChangeHeader();
protected function buildPrimitives($range_start, $range_len) {
$primitives = array();
$hunk_starts = $this->getHunkStartLines();
$mask = $this->getMask();
$gaps = $this->getGaps();
$old = $this->getOldLines();
$new = $this->getNewLines();
$old_render = $this->getOldRender();
$new_render = $this->getNewRender();
$old_comments = $this->getOldComments();
$new_comments = $this->getNewComments();
$size = count($old);
for ($ii = $range_start; $ii < $range_start + $range_len; $ii++) {
if (empty($mask[$ii])) {
list($top, $len) = array_pop($gaps);
$primitives[] = array(
'type' => 'context',
'top' => $top,
'len' => $len,
);
$ii += ($len - 1);
continue;
}
$ospec = array(
'type' => 'old',
'htype' => null,
'cursor' => $ii,
'line' => null,
'oline' => null,
'render' => null,
);
$nspec = array(
'type' => 'new',
'htype' => null,
'cursor' => $ii,
'line' => null,
'oline' => null,
'render' => null,
'copy' => null,
'coverage' => null,
);
if (isset($old[$ii])) {
$ospec['line'] = (int)$old[$ii]['line'];
$nspec['oline'] = (int)$old[$ii]['line'];
$ospec['htype'] = $old[$ii]['type'];
if (isset($old_render[$ii])) {
$ospec['render'] = $old_render[$ii];
}
}
if (isset($new[$ii])) {
$nspec['line'] = (int)$new[$ii]['line'];
$ospec['oline'] = (int)$new[$ii]['line'];
$nspec['htype'] = $new[$ii]['type'];
if (isset($new_render[$ii])) {
$nspec['render'] = $new_render[$ii];
}
}
if (isset($hunk_starts[$ospec['line']])) {
$primitives[] = array(
'type' => 'no-context',
);
}
$primitives[] = $ospec;
$primitives[] = $nspec;
if ($ospec['line'] !== null && isset($old_comments[$ospec['line']])) {
foreach ($old_comments[$ospec['line']] as $comment) {
$primitives[] = array(
'type' => 'inline',
'comment' => $comment,
'right' => false,
);
}
}
if ($nspec['line'] !== null && isset($new_comments[$nspec['line']])) {
foreach ($new_comments[$nspec['line']] as $comment) {
$primitives[] = array(
'type' => 'inline',
'comment' => $comment,
'right' => true,
);
}
}
if ($hunk_starts && ($ii == $size - 1)) {
$primitives[] = array(
'type' => 'no-context',
);
}
}
if ($this->isOneUpRenderer()) {
$primitives = $this->processPrimitivesForOneUp($primitives);
}
return $primitives;
}
private function processPrimitivesForOneUp(array $primitives) {
// Primitives come out of buildPrimitives() in two-up format, because it
// is the most general, flexible format. To put them into one-up format,
// we need to filter and reorder them. In particular:
//
// - We discard unchanged lines in the old file; in one-up format, we
// render them only once.
// - We group contiguous blocks of old-modified and new-modified lines, so
// they render in "block of old, block of new" order instead of
// alternating old and new lines.
$out = array();
$old_buf = array();
$new_buf = array();
foreach ($primitives as $primitive) {
$type = $primitive['type'];
if ($type == 'old') {
if (!$primitive['htype']) {
// This is a line which appears in both the old file and the new
// file, or the spacer corresponding to a line added in the new file.
// Ignore it when rendering a one-up diff.
continue;
}
$old_buf[] = $primitive;
} else if ($type == 'new') {
if ($primitive['line'] === null) {
// This is an empty spacer corresponding to a line removed from the
// old file. Ignore it when rendering a one-up diff.
continue;
}
if (!$primitive['htype']) {
// If this line is the same in both versions of the file, put it in
// the old line buffer. This makes sure inlines on old, unchanged
// lines end up in the right place.
// First, we need to flush the line buffers if they're not empty.
if ($old_buf) {
$out[] = $old_buf;
$old_buf = array();
}
if ($new_buf) {
$out[] = $new_buf;
$new_buf = array();
}
$old_buf[] = $primitive;
} else {
$new_buf[] = $primitive;
}
} else if ($type == 'context' || $type == 'no-context') {
$out[] = $old_buf;
$out[] = $new_buf;
$old_buf = array();
$new_buf = array();
$out[] = array($primitive);
} else if ($type == 'inline') {
// If this inline is on the left side, put it after the old lines.
if (!$primitive['right']) {
$out[] = $old_buf;
$out[] = array($primitive);
$old_buf = array();
} else {
$out[] = $old_buf;
$out[] = $new_buf;
$out[] = array($primitive);
$old_buf = array();
$new_buf = array();
}
} else {
throw new Exception(pht("Unknown primitive type '%s'!", $primitive));
}
}
$out[] = $old_buf;
$out[] = $new_buf;
$out = array_mergev($out);
return $out;
}
protected function getChangesetProperties($changeset) {
$old = $changeset->getOldProperties();
$new = $changeset->getNewProperties();
// When adding files, don't show the uninteresting 644 filemode change.
if ($changeset->getChangeType() == DifferentialChangeType::TYPE_ADD &&
$new == array('unix:filemode' => '100644')) {
unset($new['unix:filemode']);
}
// Likewise when removing files.
if ($changeset->getChangeType() == DifferentialChangeType::TYPE_DELETE &&
$old == array('unix:filemode' => '100644')) {
unset($old['unix:filemode']);
}
$metadata = $changeset->getMetadata();
if ($this->hasOldFile()) {
$file = $this->getOldFile();
if ($file->getImageWidth()) {
$dimensions = $file->getImageWidth().'x'.$file->getImageHeight();
$old['file:dimensions'] = $dimensions;
}
$old['file:mimetype'] = $file->getMimeType();
$old['file:size'] = phutil_format_bytes($file->getByteSize());
} else {
$old['file:mimetype'] = idx($metadata, 'old:file:mime-type');
$size = idx($metadata, 'old:file:size');
if ($size !== null) {
$old['file:size'] = phutil_format_bytes($size);
}
}
if ($this->hasNewFile()) {
$file = $this->getNewFile();
if ($file->getImageWidth()) {
$dimensions = $file->getImageWidth().'x'.$file->getImageHeight();
$new['file:dimensions'] = $dimensions;
}
$new['file:mimetype'] = $file->getMimeType();
$new['file:size'] = phutil_format_bytes($file->getByteSize());
} else {
$new['file:mimetype'] = idx($metadata, 'new:file:mime-type');
$size = idx($metadata, 'new:file:size');
if ($size !== null) {
$new['file:size'] = phutil_format_bytes($size);
}
}
return array($old, $new);
}
public function renderUndoTemplates() {
$views = array(
'l' => id(new PHUIDiffInlineCommentUndoView())->setIsOnRight(false),
'r' => id(new PHUIDiffInlineCommentUndoView())->setIsOnRight(true),
);
foreach ($views as $key => $view) {
$scaffold = $this->getRowScaffoldForInline($view);
$views[$key] = id(new PHUIDiffInlineCommentTableScaffold())
->addRowScaffold($scaffold);
}
return $views;
}
+ final protected function getScopeEngine() {
+ if ($this->scopeEngine === false) {
+ $hunk_starts = $this->getHunkStartLines();
+
+ // If this change is missing context, don't try to identify scopes, since
+ // we won't really be able to get anywhere.
+ $has_multiple_hunks = (count($hunk_starts) > 1);
+ $has_offset_hunks = (head_key($hunk_starts) != 1);
+ $missing_context = ($has_multiple_hunks || $has_offset_hunks);
+
+ if ($missing_context) {
+ $scope_engine = null;
+ } else {
+ $line_map = $this->getNewLineTextMap();
+ $scope_engine = id(new PhabricatorDiffScopeEngine())
+ ->setLineTextMap($line_map);
+ }
+
+ $this->scopeEngine = $scope_engine;
+ }
+
+ return $this->scopeEngine;
+ }
+
+ private function getNewLineTextMap() {
+ $new = $this->getNewLines();
+
+ $text_map = array();
+ foreach ($new as $new_line) {
+ if (!isset($new_line['line'])) {
+ continue;
+ }
+ $text_map[$new_line['line']] = $new_line['text'];
+ }
+
+ return $text_map;
+ }
+
}
diff --git a/src/applications/differential/render/DifferentialChangesetTestRenderer.php b/src/applications/differential/render/DifferentialChangesetTestRenderer.php
index a0d1fad0e..e2bd3f53e 100644
--- a/src/applications/differential/render/DifferentialChangesetTestRenderer.php
+++ b/src/applications/differential/render/DifferentialChangesetTestRenderer.php
@@ -1,143 +1,147 @@
<?php
abstract class DifferentialChangesetTestRenderer
extends DifferentialChangesetRenderer {
protected function renderChangeTypeHeader($force) {
$changeset = $this->getChangeset();
$old = nonempty($changeset->getOldFile(), '-');
$current = nonempty($changeset->getFilename(), '-');
$away = nonempty(implode(', ', $changeset->getAwayPaths()), '-');
$ctype = $changeset->getChangeType();
$ftype = $changeset->getFileType();
$force = ($force ? '(forced)' : '(unforced)');
return "CTYPE {$ctype} {$ftype} {$force}\n".
"{$old}\n".
"{$current}\n".
"{$away}\n";
}
protected function renderUndershieldHeader() {
return null;
}
public function renderShield($message, $force = 'default') {
return "SHIELD ({$force}) {$message}\n";
}
protected function renderPropertyChangeHeader() {
$changeset = $this->getChangeset();
list($old, $new) = $this->getChangesetProperties($changeset);
foreach (array_keys($old) as $key) {
if ($old[$key] === idx($new, $key)) {
unset($old[$key]);
unset($new[$key]);
}
}
if (!$old && !$new) {
return null;
}
$props = '';
foreach ($old as $key => $value) {
$props .= "P - {$key} {$value}~\n";
}
foreach ($new as $key => $value) {
$props .= "P + {$key} {$value}~\n";
}
return "PROPERTIES\n".$props;
}
public function renderTextChange(
$range_start,
$range_len,
$rows) {
$out = array();
$any_old = false;
$any_new = false;
$primitives = $this->buildPrimitives($range_start, $range_len);
foreach ($primitives as $p) {
$type = $p['type'];
switch ($type) {
case 'old':
case 'new':
if ($type == 'old') {
$any_old = true;
}
if ($type == 'new') {
$any_new = true;
}
$num = nonempty($p['line'], '-');
$render = $p['render'];
$htype = nonempty($p['htype'], '.');
// TODO: This should probably happen earlier, whenever we deal with
// \r and \t normalization?
$render = str_replace(
array(
"\r",
"\n",
),
array(
'\\r',
'\\n',
),
$render);
$render = str_replace(
array(
'<span class="bright">',
'</span>',
+ '<span class="depth-out">',
+ '<span class="depth-in">',
),
array(
'{(',
')}',
+ '{<',
+ '{>',
),
$render);
$render = html_entity_decode($render, ENT_QUOTES);
$t = ($type == 'old') ? 'O' : 'N';
$out[] = "{$t} {$num} {$htype} {$render}~";
break;
case 'no-context':
$out[] = 'X <MISSING-CONTEXT>';
break;
default:
$out[] = $type;
break;
}
}
if (!$any_old) {
$out[] = 'O X <EMPTY>';
}
if (!$any_new) {
$out[] = 'N X <EMPTY>';
}
$out = implode("\n", $out)."\n";
- return $out;
+ return phutil_safe_html($out);
}
public function renderFileChange(
$old_file = null,
$new_file = null,
$id = 0,
$vs = 0) {
throw new PhutilMethodNotImplementedException();
}
}
diff --git a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php
index 5d476f513..d803e92c6 100644
--- a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php
+++ b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php
@@ -1,389 +1,470 @@
<?php
final class DifferentialChangesetTwoUpRenderer
extends DifferentialChangesetHTMLRenderer {
+ private $newOffsetMap;
+
public function isOneUpRenderer() {
return false;
}
protected function getRendererTableClass() {
return 'diff-2up';
}
public function getRendererKey() {
return '2up';
}
protected function renderColgroup() {
return phutil_tag('colgroup', array(), array(
phutil_tag('col', array('class' => 'num')),
phutil_tag('col', array('class' => 'left')),
phutil_tag('col', array('class' => 'num')),
phutil_tag('col', array('class' => 'copy')),
phutil_tag('col', array('class' => 'right')),
phutil_tag('col', array('class' => 'cov')),
));
}
public function renderTextChange(
$range_start,
$range_len,
$rows) {
$hunk_starts = $this->getHunkStartLines();
$context_not_available = null;
if ($hunk_starts) {
$context_not_available = javelin_tag(
'tr',
array(
'sigil' => 'context-target',
),
phutil_tag(
'td',
array(
'colspan' => 6,
'class' => 'show-more',
),
pht('Context not available.')));
}
$html = array();
$old_lines = $this->getOldLines();
$new_lines = $this->getNewLines();
$gaps = $this->getGaps();
$reference = $this->getRenderingReference();
list($left_prefix, $right_prefix) = $this->getLineIDPrefixes();
$changeset = $this->getChangeset();
$copy_lines = idx($changeset->getMetadata(), 'copy:lines', array());
$highlight_old = $this->getHighlightOld();
$highlight_new = $this->getHighlightNew();
$old_render = $this->getOldRender();
$new_render = $this->getNewRender();
$original_left = $this->getOriginalOld();
$original_right = $this->getOriginalNew();
- $depths = $this->getDepths();
$mask = $this->getMask();
+ $scope_engine = $this->getScopeEngine();
+ $offset_map = null;
+ $depth_only = $this->getDepthOnlyLines();
+
for ($ii = $range_start; $ii < $range_start + $range_len; $ii++) {
if (empty($mask[$ii])) {
// If we aren't going to show this line, we've just entered a gap.
// Pop information about the next gap off the $gaps stack and render
// an appropriate "Show more context" element. This branch eventually
// increments $ii by the entire size of the gap and then continues
// the loop.
$gap = array_pop($gaps);
$top = $gap[0];
$len = $gap[1];
$contents = $this->renderShowContextLinks($top, $len, $rows);
$is_last_block = false;
if ($ii + $len >= $rows) {
$is_last_block = true;
}
- $context = null;
+ $context_text = null;
$context_line = null;
- if (!$is_last_block && $depths[$ii + $len]) {
- for ($l = $ii + $len - 1; $l >= $ii; $l--) {
- $line = $new_lines[$l]['text'];
- if ($depths[$l] < $depths[$ii + $len] && trim($line) != '') {
- $context = $new_render[$l];
- $context_line = $new_lines[$l]['line'];
- break;
+ if (!$is_last_block && $scope_engine) {
+ $target_line = $new_lines[$ii + $len]['line'];
+ $context_line = $scope_engine->getScopeStart($target_line);
+ if ($context_line !== null) {
+ // The scope engine returns a line number in the file. We need
+ // to map that back to a display offset in the diff.
+ if (!$offset_map) {
+ $offset_map = $this->getNewLineToOffsetMap();
}
+ $offset = $offset_map[$context_line];
+ $context_text = $new_render[$offset];
}
}
$container = javelin_tag(
'tr',
array(
'sigil' => 'context-target',
),
array(
phutil_tag(
'td',
array(
- 'colspan' => 2,
+ 'class' => 'show-context-line n left-context',
+ )),
+ phutil_tag(
+ 'td',
+ array(
'class' => 'show-more',
),
$contents),
phutil_tag(
- 'th',
+ 'td',
array(
- 'class' => 'show-context-line',
- ),
- $context_line ? (int)$context_line : null),
+ 'class' => 'show-context-line n',
+ 'data-n' => $context_line,
+ )),
phutil_tag(
'td',
array(
'colspan' => 3,
'class' => 'show-context',
),
// TODO: [HTML] Escaping model here isn't ideal.
- phutil_safe_html($context)),
+ phutil_safe_html($context_text)),
));
$html[] = $container;
$ii += ($len - 1);
continue;
}
$o_num = null;
$o_classes = '';
$o_text = null;
if (isset($old_lines[$ii])) {
$o_num = $old_lines[$ii]['line'];
$o_text = isset($old_render[$ii]) ? $old_render[$ii] : null;
if ($old_lines[$ii]['type']) {
if ($old_lines[$ii]['type'] == '\\') {
$o_text = $old_lines[$ii]['text'];
$o_class = 'comment';
} else if ($original_left && !isset($highlight_old[$o_num])) {
$o_class = 'old-rebase';
} else if (empty($new_lines[$ii])) {
$o_class = 'old old-full';
} else {
- $o_class = 'old';
+ if (isset($depth_only[$ii])) {
+ if ($depth_only[$ii] == '>') {
+ // When a line has depth-only change, we only highlight the
+ // left side of the diff if the depth is decreasing. When the
+ // depth is increasing, the ">>" marker on the right hand side
+ // of the diff generally provides enough visibility on its own.
+
+ $o_class = '';
+ } else {
+ $o_class = 'old';
+ }
+ } else {
+ $o_class = 'old';
+ }
}
$o_classes = $o_class;
}
}
$n_copy = hsprintf('<td class="copy" />');
$n_cov = null;
$n_colspan = 2;
$n_classes = '';
$n_num = null;
$n_text = null;
if (isset($new_lines[$ii])) {
$n_num = $new_lines[$ii]['line'];
$n_text = isset($new_render[$ii]) ? $new_render[$ii] : null;
$coverage = $this->getCodeCoverage();
if ($coverage !== null) {
if (empty($coverage[$n_num - 1])) {
$cov_class = 'N';
} else {
$cov_class = $coverage[$n_num - 1];
}
$cov_class = 'cov-'.$cov_class;
$n_cov = phutil_tag('td', array('class' => "cov {$cov_class}"));
$n_colspan--;
}
if ($new_lines[$ii]['type']) {
if ($new_lines[$ii]['type'] == '\\') {
$n_text = $new_lines[$ii]['text'];
$n_class = 'comment';
} else if ($original_right && !isset($highlight_new[$n_num])) {
$n_class = 'new-rebase';
} else if (empty($old_lines[$ii])) {
$n_class = 'new new-full';
} else {
- $n_class = 'new';
+ // When a line has a depth-only change, never highlight it on
+ // the right side. The ">>" marker generally provides enough
+ // visibility on its own for indent depth increases, and the left
+ // side is still highlighted for indent depth decreases.
+
+ if (isset($depth_only[$ii])) {
+ $n_class = '';
+ } else {
+ $n_class = 'new';
+ }
}
$n_classes = $n_class;
- if ($new_lines[$ii]['type'] == '\\' || !isset($copy_lines[$n_num])) {
- $n_copy = phutil_tag('td', array('class' => "copy {$n_class}"));
+ $not_copied =
+ // If this line only changed depth, copy markers are pointless.
+ (!isset($copy_lines[$n_num])) ||
+ (isset($depth_only[$ii])) ||
+ ($new_lines[$ii]['type'] == '\\');
+
+ if ($not_copied) {
+ $n_copy = phutil_tag('td', array('class' => 'copy'));
} else {
list($orig_file, $orig_line, $orig_type) = $copy_lines[$n_num];
$title = ($orig_type == '-' ? 'Moved' : 'Copied').' from ';
if ($orig_file == '') {
$title .= "line {$orig_line}";
} else {
$title .=
basename($orig_file).
":{$orig_line} in dir ".
dirname('/'.$orig_file);
}
$class = ($orig_type == '-' ? 'new-move' : 'new-copy');
$n_copy = javelin_tag(
'td',
array(
'meta' => array(
'msg' => $title,
),
'class' => 'copy '.$class,
- ),
- '');
+ ));
}
}
}
if (isset($hunk_starts[$o_num])) {
$html[] = $context_not_available;
}
if ($o_num && $left_prefix) {
$o_id = $left_prefix.$o_num;
} else {
$o_id = null;
}
if ($n_num && $right_prefix) {
$n_id = $right_prefix.$n_num;
} else {
$n_id = null;
}
$old_comments = $this->getOldComments();
$new_comments = $this->getNewComments();
$scaffolds = array();
if ($o_num && isset($old_comments[$o_num])) {
foreach ($old_comments[$o_num] as $comment) {
$inline = $this->buildInlineComment(
$comment,
$on_right = false);
$scaffold = $this->getRowScaffoldForInline($inline);
if ($n_num && isset($new_comments[$n_num])) {
foreach ($new_comments[$n_num] as $key => $new_comment) {
if ($comment->isCompatible($new_comment)) {
$companion = $this->buildInlineComment(
$new_comment,
$on_right = true);
$scaffold->addInlineView($companion);
unset($new_comments[$n_num][$key]);
break;
}
}
}
$scaffolds[] = $scaffold;
}
}
if ($n_num && isset($new_comments[$n_num])) {
foreach ($new_comments[$n_num] as $comment) {
$inline = $this->buildInlineComment(
$comment,
$on_right = true);
$scaffolds[] = $this->getRowScaffoldForInline($inline);
}
}
- // NOTE: This is a unicode zero-width space, which we use as a hint when
- // intercepting 'copy' events to make sure sensible text ends up on the
- // clipboard. See the 'phabricator-oncopy' behavior.
- $zero_space = "\xE2\x80\x8B";
+ $old_number = phutil_tag(
+ 'td',
+ array(
+ 'id' => $o_id,
+ 'class' => $o_classes.' n',
+ 'data-n' => $o_num,
+ ));
+
+ $new_number = phutil_tag(
+ 'td',
+ array(
+ 'id' => $n_id,
+ 'class' => $n_classes.' n',
+ 'data-n' => $n_num,
+ ));
$html[] = phutil_tag('tr', array(), array(
- phutil_tag('th', array('id' => $o_id, 'class' => $o_classes), $o_num),
- phutil_tag('td', array('class' => $o_classes), $o_text),
- phutil_tag('th', array('id' => $n_id, 'class' => $n_classes), $n_num),
+ $old_number,
+ phutil_tag(
+ 'td',
+ array(
+ 'class' => $o_classes,
+ 'data-copy-mode' => 'copy-l',
+ ),
+ $o_text),
+ $new_number,
$n_copy,
phutil_tag(
'td',
- array('class' => $n_classes, 'colspan' => $n_colspan),
array(
- phutil_tag('span', array('class' => 'zwsp'), $zero_space),
- $n_text,
- )),
+ 'class' => $n_classes,
+ 'colspan' => $n_colspan,
+ 'data-copy-mode' => 'copy-r',
+ ),
+ $n_text),
$n_cov,
));
if ($context_not_available && ($ii == $rows - 1)) {
$html[] = $context_not_available;
}
foreach ($scaffolds as $scaffold) {
$html[] = $scaffold;
}
}
return $this->wrapChangeInTable(phutil_implode_html('', $html));
}
public function renderFileChange(
$old_file = null,
$new_file = null,
$id = 0,
$vs = 0) {
$old = null;
if ($old_file) {
$old = $this->renderImageStage($old_file);
}
$new = null;
if ($new_file) {
$new = $this->renderImageStage($new_file);
}
// If we don't have an explicit "vs" changeset, it's the left side of the
// "id" changeset.
if (!$vs) {
$vs = $id;
}
$html_old = array();
$html_new = array();
foreach ($this->getOldComments() as $on_line => $comment_group) {
foreach ($comment_group as $comment) {
$inline = $this->buildInlineComment(
$comment,
$on_right = false);
$html_old[] = $this->getRowScaffoldForInline($inline);
}
}
foreach ($this->getNewComments() as $lin_line => $comment_group) {
foreach ($comment_group as $comment) {
$inline = $this->buildInlineComment(
$comment,
$on_right = true);
$html_new[] = $this->getRowScaffoldForInline($inline);
}
}
if (!$old) {
$th_old = phutil_tag('th', array());
} else {
$th_old = phutil_tag('th', array('id' => "C{$vs}OL1"), 1);
}
if (!$new) {
$th_new = phutil_tag('th', array());
} else {
$th_new = phutil_tag('th', array('id' => "C{$id}NL1"), 1);
}
$output = hsprintf(
'<tr class="differential-image-diff">'.
'%s'.
'<td class="differential-old-image">%s</td>'.
'%s'.
'<td class="differential-new-image" colspan="3">%s</td>'.
'</tr>'.
'%s'.
'%s',
$th_old,
$old,
$th_new,
$new,
phutil_implode_html('', $html_old),
phutil_implode_html('', $html_new));
$output = $this->wrapChangeInTable($output);
return $this->renderChangesetTable($output);
}
public function getRowScaffoldForInline(PHUIDiffInlineCommentView $view) {
return id(new PHUIDiffTwoUpInlineCommentRowScaffold())
->addInlineView($view);
}
+ private function getNewLineToOffsetMap() {
+ if ($this->newOffsetMap === null) {
+ $new = $this->getNewLines();
+
+ $map = array();
+ foreach ($new as $offset => $new_line) {
+ if ($new_line['line'] === null) {
+ continue;
+ }
+ $map[$new_line['line']] = $offset;
+ }
+
+ $this->newOffsetMap = $map;
+ }
+
+ return $this->newOffsetMap;
+ }
+
+ protected function getTableSigils() {
+ return array(
+ 'intercept-copy',
+ );
+ }
+
}
diff --git a/src/applications/differential/storage/DifferentialChangeset.php b/src/applications/differential/storage/DifferentialChangeset.php
index 00af84bc4..03400d7a1 100644
--- a/src/applications/differential/storage/DifferentialChangeset.php
+++ b/src/applications/differential/storage/DifferentialChangeset.php
@@ -1,440 +1,429 @@
<?php
final class DifferentialChangeset
extends DifferentialDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
protected $diffID;
protected $oldFile;
protected $filename;
protected $awayPaths;
protected $changeType;
protected $fileType;
protected $metadata = array();
protected $oldProperties;
protected $newProperties;
protected $addLines;
protected $delLines;
private $unsavedHunks = array();
private $hunks = self::ATTACHABLE;
private $diff = self::ATTACHABLE;
const TABLE_CACHE = 'differential_changeset_parse_cache';
const METADATA_TRUSTED_ATTRIBUTES = 'attributes.trusted';
const METADATA_UNTRUSTED_ATTRIBUTES = 'attributes.untrusted';
const METADATA_EFFECT_HASH = 'hash.effect';
const ATTRIBUTE_GENERATED = 'generated';
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
'oldProperties' => self::SERIALIZATION_JSON,
'newProperties' => self::SERIALIZATION_JSON,
'awayPaths' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'oldFile' => 'bytes?',
'filename' => 'bytes',
'changeType' => 'uint32',
'fileType' => 'uint32',
'addLines' => 'uint32',
'delLines' => 'uint32',
// T6203/NULLABILITY
// These should all be non-nullable, and store reasonable default
// JSON values if empty.
'awayPaths' => 'text?',
'metadata' => 'text?',
'oldProperties' => 'text?',
'newProperties' => 'text?',
),
self::CONFIG_KEY_SCHEMA => array(
'diffID' => array(
'columns' => array('diffID'),
),
),
) + parent::getConfiguration();
}
public function getAffectedLineCount() {
return $this->getAddLines() + $this->getDelLines();
}
public function attachHunks(array $hunks) {
assert_instances_of($hunks, 'DifferentialHunk');
$this->hunks = $hunks;
return $this;
}
public function getHunks() {
return $this->assertAttached($this->hunks);
}
public function getDisplayFilename() {
$name = $this->getFilename();
if ($this->getFileType() == DifferentialChangeType::FILE_DIRECTORY) {
$name .= '/';
}
return $name;
}
public function getOwnersFilename() {
// TODO: For Subversion, we should adjust these paths to be relative to
// the repository root where possible.
$path = $this->getFilename();
if (!isset($path[0])) {
return '/';
}
if ($path[0] != '/') {
$path = '/'.$path;
}
return $path;
}
public function addUnsavedHunk(DifferentialHunk $hunk) {
if ($this->hunks === self::ATTACHABLE) {
$this->hunks = array();
}
$this->hunks[] = $hunk;
$this->unsavedHunks[] = $hunk;
return $this;
}
public function save() {
$this->openTransaction();
$ret = parent::save();
foreach ($this->unsavedHunks as $hunk) {
$hunk->setChangesetID($this->getID());
$hunk->save();
}
$this->saveTransaction();
return $ret;
}
public function delete() {
$this->openTransaction();
$hunks = id(new DifferentialHunk())->loadAllWhere(
'changesetID = %d',
$this->getID());
foreach ($hunks as $hunk) {
$hunk->delete();
}
$this->unsavedHunks = array();
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE id = %d',
self::TABLE_CACHE,
$this->getID());
$ret = parent::delete();
$this->saveTransaction();
return $ret;
}
/**
* Test if this changeset and some other changeset put the affected file in
* the same state.
*
* @param DifferentialChangeset Changeset to compare against.
* @return bool True if the two changesets have the same effect.
*/
public function hasSameEffectAs(DifferentialChangeset $other) {
if ($this->getFilename() !== $other->getFilename()) {
return false;
}
$hash_key = self::METADATA_EFFECT_HASH;
$u_hash = $this->getChangesetMetadata($hash_key);
if ($u_hash === null) {
return false;
}
$v_hash = $other->getChangesetMetadata($hash_key);
if ($v_hash === null) {
return false;
}
if ($u_hash !== $v_hash) {
return false;
}
// Make sure the final states for the file properties (like the "+x"
// executable bit) match one another.
$u_props = $this->getNewProperties();
$v_props = $other->getNewProperties();
ksort($u_props);
ksort($v_props);
if ($u_props !== $v_props) {
return false;
}
return true;
}
public function getSortKey() {
$sort_key = $this->getFilename();
// Sort files with ".h" in them first, so headers (.h, .hpp) come before
// implementations (.c, .cpp, .cs).
$sort_key = str_replace('.h', '.!h', $sort_key);
return $sort_key;
}
public function makeNewFile() {
$file = mpull($this->getHunks(), 'makeNewFile');
return implode('', $file);
}
public function makeOldFile() {
$file = mpull($this->getHunks(), 'makeOldFile');
return implode('', $file);
}
public function makeChangesWithContext($num_lines = 3) {
$with_context = array();
foreach ($this->getHunks() as $hunk) {
$context = array();
$changes = explode("\n", $hunk->getChanges());
foreach ($changes as $l => $line) {
$type = substr($line, 0, 1);
if ($type == '+' || $type == '-') {
$context += array_fill($l - $num_lines, 2 * $num_lines + 1, true);
}
}
$with_context[] = array_intersect_key($changes, $context);
}
return array_mergev($with_context);
}
public function getAnchorName() {
return 'change-'.PhabricatorHash::digestForAnchor($this->getFilename());
}
public function getAbsoluteRepositoryPath(
PhabricatorRepository $repository = null,
DifferentialDiff $diff = null) {
$base = '/';
if ($diff && $diff->getSourceControlPath()) {
$base = id(new PhutilURI($diff->getSourceControlPath()))->getPath();
}
$path = $this->getFilename();
$path = rtrim($base, '/').'/'.ltrim($path, '/');
$svn = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN;
if ($repository && $repository->getVersionControlSystem() == $svn) {
$prefix = $repository->getDetail('remote-uri');
$prefix = id(new PhutilURI($prefix))->getPath();
if (!strncmp($path, $prefix, strlen($prefix))) {
$path = substr($path, strlen($prefix));
}
$path = '/'.ltrim($path, '/');
}
return $path;
}
- public function getWhitespaceMatters() {
- $config = PhabricatorEnv::getEnvConfig('differential.whitespace-matters');
- foreach ($config as $regexp) {
- if (preg_match($regexp, $this->getFilename())) {
- return true;
- }
- }
-
- return false;
- }
-
public function attachDiff(DifferentialDiff $diff) {
$this->diff = $diff;
return $this;
}
public function getDiff() {
return $this->assertAttached($this->diff);
}
public function newFileTreeIcon() {
$file_type = $this->getFileType();
$change_type = $this->getChangeType();
$change_icons = array(
DifferentialChangeType::TYPE_DELETE => 'fa-file-o',
);
if (isset($change_icons[$change_type])) {
$icon = $change_icons[$change_type];
} else {
$icon = DifferentialChangeType::getIconForFileType($file_type);
}
$change_colors = array(
DifferentialChangeType::TYPE_ADD => 'green',
DifferentialChangeType::TYPE_DELETE => 'red',
DifferentialChangeType::TYPE_MOVE_AWAY => 'orange',
DifferentialChangeType::TYPE_MOVE_HERE => 'orange',
DifferentialChangeType::TYPE_COPY_HERE => 'orange',
DifferentialChangeType::TYPE_MULTICOPY => 'orange',
);
$color = idx($change_colors, $change_type, 'bluetext');
return id(new PHUIIconView())
->setIcon($icon.' '.$color);
}
public function getFileTreeClass() {
switch ($this->getChangeType()) {
case DifferentialChangeType::TYPE_ADD:
return 'filetree-added';
case DifferentialChangeType::TYPE_DELETE:
return 'filetree-deleted';
case DifferentialChangeType::TYPE_MOVE_AWAY:
case DifferentialChangeType::TYPE_MOVE_HERE:
case DifferentialChangeType::TYPE_COPY_HERE:
case DifferentialChangeType::TYPE_MULTICOPY:
return 'filetree-movecopy';
}
return null;
}
public function setChangesetMetadata($key, $value) {
if (!is_array($this->metadata)) {
$this->metadata = array();
}
$this->metadata[$key] = $value;
return $this;
}
public function getChangesetMetadata($key, $default = null) {
if (!is_array($this->metadata)) {
return $default;
}
return idx($this->metadata, $key, $default);
}
private function setInternalChangesetAttribute($trusted, $key, $value) {
if ($trusted) {
$meta_key = self::METADATA_TRUSTED_ATTRIBUTES;
} else {
$meta_key = self::METADATA_UNTRUSTED_ATTRIBUTES;
}
$attributes = $this->getChangesetMetadata($meta_key, array());
$attributes[$key] = $value;
$this->setChangesetMetadata($meta_key, $attributes);
return $this;
}
private function getInternalChangesetAttributes($trusted) {
if ($trusted) {
$meta_key = self::METADATA_TRUSTED_ATTRIBUTES;
} else {
$meta_key = self::METADATA_UNTRUSTED_ATTRIBUTES;
}
return $this->getChangesetMetadata($meta_key, array());
}
public function setTrustedChangesetAttribute($key, $value) {
return $this->setInternalChangesetAttribute(true, $key, $value);
}
public function getTrustedChangesetAttributes() {
return $this->getInternalChangesetAttributes(true);
}
public function getTrustedChangesetAttribute($key, $default = null) {
$map = $this->getTrustedChangesetAttributes();
return idx($map, $key, $default);
}
public function setUntrustedChangesetAttribute($key, $value) {
return $this->setInternalChangesetAttribute(false, $key, $value);
}
public function getUntrustedChangesetAttributes() {
return $this->getInternalChangesetAttributes(false);
}
public function getUntrustedChangesetAttribute($key, $default = null) {
$map = $this->getUntrustedChangesetAttributes();
return idx($map, $key, $default);
}
public function getChangesetAttributes() {
// Prefer trusted values over untrusted values when both exist.
return
$this->getTrustedChangesetAttributes() +
$this->getUntrustedChangesetAttributes();
}
public function getChangesetAttribute($key, $default = null) {
$map = $this->getChangesetAttributes();
return idx($map, $key, $default);
}
public function isGeneratedChangeset() {
return $this->getChangesetAttribute(self::ATTRIBUTE_GENERATED);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return $this->getDiff()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getDiff()->hasAutomaticCapability($capability, $viewer);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$hunks = id(new DifferentialHunk())->loadAllWhere(
'changesetID = %d',
$this->getID());
foreach ($hunks as $hunk) {
$engine->destroyObject($hunk);
}
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/differential/storage/DifferentialDiff.php b/src/applications/differential/storage/DifferentialDiff.php
index e4c33dc76..a39610c54 100644
--- a/src/applications/differential/storage/DifferentialDiff.php
+++ b/src/applications/differential/storage/DifferentialDiff.php
@@ -1,810 +1,811 @@
<?php
final class DifferentialDiff
extends DifferentialDAO
implements
PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface,
HarbormasterBuildableInterface,
HarbormasterCircleCIBuildableInterface,
HarbormasterBuildkiteBuildableInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface,
PhabricatorConduitResultInterface {
protected $revisionID;
protected $authorPHID;
protected $repositoryPHID;
protected $commitPHID;
protected $sourceMachine;
protected $sourcePath;
protected $sourceControlSystem;
protected $sourceControlBaseRevision;
protected $sourceControlPath;
protected $lintStatus;
protected $unitStatus;
protected $lineCount;
protected $branch;
protected $bookmark;
protected $creationMethod;
protected $repositoryUUID;
protected $description;
protected $viewPolicy;
private $unsavedChangesets = array();
private $changesets = self::ATTACHABLE;
private $revision = self::ATTACHABLE;
private $properties = array();
private $buildable = self::ATTACHABLE;
private $unitMessages = self::ATTACHABLE;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'revisionID' => 'id?',
'authorPHID' => 'phid?',
'repositoryPHID' => 'phid?',
'sourceMachine' => 'text255?',
'sourcePath' => 'text255?',
'sourceControlSystem' => 'text64?',
'sourceControlBaseRevision' => 'text255?',
'sourceControlPath' => 'text255?',
'lintStatus' => 'uint32',
'unitStatus' => 'uint32',
'lineCount' => 'uint32',
'branch' => 'text255?',
'bookmark' => 'text255?',
'repositoryUUID' => 'text64?',
'commitPHID' => 'phid?',
// T6203/NULLABILITY
// These should be non-null; all diffs should have a creation method
// and the description should just be empty.
'creationMethod' => 'text255?',
'description' => 'text255?',
),
self::CONFIG_KEY_SCHEMA => array(
'revisionID' => array(
'columns' => array('revisionID'),
),
'key_commit' => array(
'columns' => array('commitPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
DifferentialDiffPHIDType::TYPECONST);
}
public function addUnsavedChangeset(DifferentialChangeset $changeset) {
if ($this->changesets === null) {
$this->changesets = array();
}
$this->unsavedChangesets[] = $changeset;
$this->changesets[] = $changeset;
return $this;
}
public function attachChangesets(array $changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
$this->changesets = $changesets;
return $this;
}
public function getChangesets() {
return $this->assertAttached($this->changesets);
}
public function loadChangesets() {
if (!$this->getID()) {
return array();
}
$changesets = id(new DifferentialChangeset())->loadAllWhere(
'diffID = %d',
$this->getID());
foreach ($changesets as $changeset) {
$changeset->attachDiff($this);
}
return $changesets;
}
public function save() {
$this->openTransaction();
$ret = parent::save();
foreach ($this->unsavedChangesets as $changeset) {
$changeset->setDiffID($this->getID());
$changeset->save();
}
$this->saveTransaction();
return $ret;
}
public static function initializeNewDiff(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorDifferentialApplication'))
->executeOne();
$view_policy = $app->getPolicy(
DifferentialDefaultViewCapability::CAPABILITY);
$diff = id(new DifferentialDiff())
->setViewPolicy($view_policy);
return $diff;
}
public static function newFromRawChanges(
PhabricatorUser $actor,
array $changes) {
assert_instances_of($changes, 'ArcanistDiffChange');
$diff = self::initializeNewDiff($actor);
return self::buildChangesetsFromRawChanges($diff, $changes);
}
public static function newEphemeralFromRawChanges(array $changes) {
assert_instances_of($changes, 'ArcanistDiffChange');
$diff = id(new DifferentialDiff())->makeEphemeral();
return self::buildChangesetsFromRawChanges($diff, $changes);
}
private static function buildChangesetsFromRawChanges(
DifferentialDiff $diff,
array $changes) {
// There may not be any changes; initialize the changesets list so that
// we don't throw later when accessing it.
$diff->attachChangesets(array());
$lines = 0;
foreach ($changes as $change) {
if ($change->getType() == ArcanistDiffChangeType::TYPE_MESSAGE) {
// If a user pastes a diff into Differential which includes a commit
// message (e.g., they ran `git show` to generate it), discard that
// change when constructing a DifferentialDiff.
continue;
}
$changeset = new DifferentialChangeset();
$add_lines = 0;
$del_lines = 0;
$first_line = PHP_INT_MAX;
$hunks = $change->getHunks();
if ($hunks) {
foreach ($hunks as $hunk) {
$dhunk = new DifferentialHunk();
$dhunk->setOldOffset($hunk->getOldOffset());
$dhunk->setOldLen($hunk->getOldLength());
$dhunk->setNewOffset($hunk->getNewOffset());
$dhunk->setNewLen($hunk->getNewLength());
$dhunk->setChanges($hunk->getCorpus());
$changeset->addUnsavedHunk($dhunk);
$add_lines += $hunk->getAddLines();
$del_lines += $hunk->getDelLines();
$added_lines = $hunk->getChangedLines('new');
if ($added_lines) {
$first_line = min($first_line, head_key($added_lines));
}
}
$lines += $add_lines + $del_lines;
} else {
// This happens when you add empty files.
$changeset->attachHunks(array());
}
$metadata = $change->getAllMetadata();
if ($first_line != PHP_INT_MAX) {
$metadata['line:first'] = $first_line;
}
$changeset->setOldFile($change->getOldPath());
$changeset->setFilename($change->getCurrentPath());
$changeset->setChangeType($change->getType());
$changeset->setFileType($change->getFileType());
$changeset->setMetadata($metadata);
$changeset->setOldProperties($change->getOldProperties());
$changeset->setNewProperties($change->getNewProperties());
$changeset->setAwayPaths($change->getAwayPaths());
$changeset->setAddLines($add_lines);
$changeset->setDelLines($del_lines);
$diff->addUnsavedChangeset($changeset);
}
$diff->setLineCount($lines);
$changesets = $diff->getChangesets();
id(new DifferentialChangesetEngine())
->rebuildChangesets($changesets);
return $diff;
}
public function getDiffDict() {
$dict = array(
'id' => $this->getID(),
'revisionID' => $this->getRevisionID(),
'dateCreated' => $this->getDateCreated(),
'dateModified' => $this->getDateModified(),
'sourceControlBaseRevision' => $this->getSourceControlBaseRevision(),
'sourceControlPath' => $this->getSourceControlPath(),
'sourceControlSystem' => $this->getSourceControlSystem(),
'branch' => $this->getBranch(),
'bookmark' => $this->getBookmark(),
'creationMethod' => $this->getCreationMethod(),
'description' => $this->getDescription(),
'unitStatus' => $this->getUnitStatus(),
'lintStatus' => $this->getLintStatus(),
'changes' => array(),
);
$dict['changes'] = $this->buildChangesList();
return $dict + $this->getDiffAuthorshipDict();
}
public function getDiffAuthorshipDict() {
$dict = array('properties' => array());
$properties = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID = %d',
$this->getID());
foreach ($properties as $property) {
$dict['properties'][$property->getName()] = $property->getData();
if ($property->getName() == 'local:commits') {
foreach ($property->getData() as $commit) {
$dict['authorName'] = $commit['author'];
$dict['authorEmail'] = idx($commit, 'authorEmail');
break;
}
}
}
return $dict;
}
public function buildChangesList() {
$changes = array();
foreach ($this->getChangesets() as $changeset) {
$hunks = array();
foreach ($changeset->getHunks() as $hunk) {
$hunks[] = array(
'oldOffset' => $hunk->getOldOffset(),
'newOffset' => $hunk->getNewOffset(),
'oldLength' => $hunk->getOldLen(),
'newLength' => $hunk->getNewLen(),
'addLines' => null,
'delLines' => null,
'isMissingOldNewline' => null,
'isMissingNewNewline' => null,
'corpus' => $hunk->getChanges(),
);
}
$change = array(
'id' => $changeset->getID(),
'metadata' => $changeset->getMetadata(),
'oldPath' => $changeset->getOldFile(),
'currentPath' => $changeset->getFilename(),
'awayPaths' => $changeset->getAwayPaths(),
'oldProperties' => $changeset->getOldProperties(),
'newProperties' => $changeset->getNewProperties(),
'type' => $changeset->getChangeType(),
'fileType' => $changeset->getFileType(),
'commitHash' => null,
'addLines' => $changeset->getAddLines(),
'delLines' => $changeset->getDelLines(),
'hunks' => $hunks,
);
$changes[] = $change;
}
return $changes;
}
public function hasRevision() {
return $this->revision !== self::ATTACHABLE;
}
public function getRevision() {
return $this->assertAttached($this->revision);
}
public function attachRevision(DifferentialRevision $revision = null) {
$this->revision = $revision;
return $this;
}
public function attachProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getProperty($key) {
return $this->assertAttachedKey($this->properties, $key);
}
public function hasDiffProperty($key) {
$properties = $this->getDiffProperties();
return array_key_exists($key, $properties);
}
public function attachDiffProperties(array $properties) {
$this->properties = $properties;
return $this;
}
public function getDiffProperties() {
return $this->assertAttached($this->properties);
}
public function attachBuildable(HarbormasterBuildable $buildable = null) {
$this->buildable = $buildable;
return $this;
}
public function getBuildable() {
return $this->assertAttached($this->buildable);
}
public function getBuildTargetPHIDs() {
$buildable = $this->getBuildable();
if (!$buildable) {
return array();
}
$target_phids = array();
foreach ($buildable->getBuilds() as $build) {
foreach ($build->getBuildTargets() as $target) {
$target_phids[] = $target->getPHID();
}
}
return $target_phids;
}
public function loadCoverageMap(PhabricatorUser $viewer) {
$target_phids = $this->getBuildTargetPHIDs();
if (!$target_phids) {
return array();
}
- $unit = id(new HarbormasterBuildUnitMessage())->loadAllWhere(
- 'buildTargetPHID IN (%Ls)',
- $target_phids);
+ $unit = id(new HarbormasterBuildUnitMessageQuery())
+ ->setViewer($viewer)
+ ->withBuildTargetPHIDs($target_phids)
+ ->execute();
$map = array();
foreach ($unit as $message) {
$coverage = $message->getProperty('coverage', array());
foreach ($coverage as $path => $coverage_data) {
$map[$path][] = $coverage_data;
}
}
foreach ($map as $path => $coverage_items) {
$map[$path] = ArcanistUnitTestResult::mergeCoverage($coverage_items);
}
return $map;
}
public function getURI() {
$id = $this->getID();
return "/differential/diff/{$id}/";
}
public function attachUnitMessages(array $unit_messages) {
$this->unitMessages = $unit_messages;
return $this;
}
public function getUnitMessages() {
return $this->assertAttached($this->unitMessages);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
if ($this->hasRevision()) {
return PhabricatorPolicies::getMostOpenPolicy();
}
return $this->viewPolicy;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->hasRevision()) {
return $this->getRevision()->hasAutomaticCapability($capability, $viewer);
}
return ($this->getAuthorPHID() == $viewer->getPHID());
}
public function describeAutomaticCapability($capability) {
if ($this->hasRevision()) {
return pht(
'This diff is attached to a revision, and inherits its policies.');
}
return pht('The author of a diff can see it.');
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
$extended = array();
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if ($this->hasRevision()) {
$extended[] = array(
$this->getRevision(),
PhabricatorPolicyCapability::CAN_VIEW,
);
}
break;
}
return $extended;
}
/* -( HarbormasterBuildableInterface )------------------------------------- */
public function getHarbormasterBuildableDisplayPHID() {
$container_phid = $this->getHarbormasterContainerPHID();
if ($container_phid) {
return $container_phid;
}
return $this->getHarbormasterBuildablePHID();
}
public function getHarbormasterBuildablePHID() {
return $this->getPHID();
}
public function getHarbormasterContainerPHID() {
if ($this->getRevisionID()) {
$revision = id(new DifferentialRevision())->load($this->getRevisionID());
if ($revision) {
return $revision->getPHID();
}
}
return null;
}
public function getBuildVariables() {
$results = array();
$results['buildable.diff'] = $this->getID();
if ($this->revisionID) {
$revision = $this->getRevision();
$results['buildable.revision'] = $revision->getID();
$repo = $revision->getRepository();
if ($repo) {
$results['repository.callsign'] = $repo->getCallsign();
$results['repository.phid'] = $repo->getPHID();
$results['repository.vcs'] = $repo->getVersionControlSystem();
$results['repository.uri'] = $repo->getPublicCloneURI();
$results['repository.staging.uri'] = $repo->getStagingURI();
$results['repository.staging.ref'] = $this->getStagingRef();
}
}
return $results;
}
public function getAvailableBuildVariables() {
return array(
'buildable.diff' =>
pht('The differential diff ID, if applicable.'),
'buildable.revision' =>
pht('The differential revision ID, if applicable.'),
'repository.callsign' =>
pht('The callsign of the repository in Phabricator.'),
'repository.phid' =>
pht('The PHID of the repository in Phabricator.'),
'repository.vcs' =>
pht('The version control system, either "svn", "hg" or "git".'),
'repository.uri' =>
pht('The URI to clone or checkout the repository from.'),
'repository.staging.uri' =>
pht('The URI of the staging repository.'),
'repository.staging.ref' =>
pht('The ref name for this change in the staging repository.'),
);
}
public function newBuildableEngine() {
return new DifferentialBuildableEngine();
}
/* -( HarbormasterCircleCIBuildableInterface )----------------------------- */
public function getCircleCIGitHubRepositoryURI() {
$diff_phid = $this->getPHID();
$repository_phid = $this->getRepositoryPHID();
if (!$repository_phid) {
throw new Exception(
pht(
'This diff ("%s") is not associated with a repository. A diff '.
'must belong to a tracked repository to be built by CircleCI.',
$diff_phid));
}
$repository = id(new PhabricatorRepositoryQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($repository_phid))
->executeOne();
if (!$repository) {
throw new Exception(
pht(
'This diff ("%s") is associated with a repository ("%s") which '.
'could not be loaded.',
$diff_phid,
$repository_phid));
}
$staging_uri = $repository->getStagingURI();
if (!$staging_uri) {
throw new Exception(
pht(
'This diff ("%s") is associated with a repository ("%s") that '.
'does not have a Staging Area configured. You must configure a '.
'Staging Area to use CircleCI integration.',
$diff_phid,
$repository_phid));
}
$path = HarbormasterCircleCIBuildStepImplementation::getGitHubPath(
$staging_uri);
if (!$path) {
throw new Exception(
pht(
'This diff ("%s") is associated with a repository ("%s") that '.
'does not have a Staging Area ("%s") that is hosted on GitHub. '.
'CircleCI can only build from GitHub, so the Staging Area for '.
'the repository must be hosted there.',
$diff_phid,
$repository_phid,
$staging_uri));
}
return $staging_uri;
}
public function getCircleCIBuildIdentifierType() {
return 'tag';
}
public function getCircleCIBuildIdentifier() {
$ref = $this->getStagingRef();
$ref = preg_replace('(^refs/tags/)', '', $ref);
return $ref;
}
/* -( HarbormasterBuildkiteBuildableInterface )---------------------------- */
public function getBuildkiteBranch() {
$ref = $this->getStagingRef();
// NOTE: Circa late January 2017, Buildkite fails with the error message
// "Tags have been disabled for this project" if we pass the "refs/tags/"
// prefix via the API and the project doesn't have GitHub tag builds
// enabled, even if GitHub builds are disabled. The tag builds fine
// without this prefix.
$ref = preg_replace('(^refs/tags/)', '', $ref);
return $ref;
}
public function getBuildkiteCommit() {
return 'HEAD';
}
public function getStagingRef() {
// TODO: We're just hoping to get lucky. Instead, `arc` should store
// where it sent changes and we should only provide staging details
// if we reasonably believe they are accurate.
return 'refs/tags/phabricator/diff/'.$this->getID();
}
public function loadTargetBranch() {
// TODO: This is sketchy, but just eat the query cost until this can get
// cleaned up.
// For now, we're only returning a target if there's exactly one and it's
// a branch, since we don't support landing to more esoteric targets like
// tags yet.
$property = id(new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$this->getID(),
'arc:onto');
if (!$property) {
return null;
}
$data = $property->getData();
if (!$data) {
return null;
}
if (!is_array($data)) {
return null;
}
if (count($data) != 1) {
return null;
}
$onto = head($data);
if (!is_array($onto)) {
return null;
}
$type = idx($onto, 'type');
if ($type != 'branch') {
return null;
}
return idx($onto, 'name');
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new DifferentialDiffEditor();
}
public function getApplicationTransactionTemplate() {
return new DifferentialDiffTransaction();
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
foreach ($this->loadChangesets() as $changeset) {
$engine->destroyObject($changeset);
}
$properties = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID = %d',
$this->getID());
foreach ($properties as $prop) {
$prop->delete();
}
$this->saveTransaction();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('revisionPHID')
->setType('phid')
->setDescription(pht('Associated revision PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('authorPHID')
->setType('phid')
->setDescription(pht('Revision author PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('repositoryPHID')
->setType('phid')
->setDescription(pht('Associated repository PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('refs')
->setType('map<string, wild>')
->setDescription(pht('List of related VCS references.')),
);
}
public function getFieldValuesForConduit() {
$refs = array();
$branch = $this->getBranch();
if (strlen($branch)) {
$refs[] = array(
'type' => 'branch',
'name' => $branch,
);
}
$onto = $this->loadTargetBranch();
if (strlen($onto)) {
$refs[] = array(
'type' => 'onto',
'name' => $onto,
);
}
$base = $this->getSourceControlBaseRevision();
if (strlen($base)) {
$refs[] = array(
'type' => 'base',
'identifier' => $base,
);
}
$bookmark = $this->getBookmark();
if (strlen($bookmark)) {
$refs[] = array(
'type' => 'bookmark',
'name' => $bookmark,
);
}
$revision_phid = null;
if ($this->getRevisionID()) {
$revision_phid = $this->getRevision()->getPHID();
}
return array(
'revisionPHID' => $revision_phid,
'authorPHID' => $this->getAuthorPHID(),
'repositoryPHID' => $this->getRepositoryPHID(),
'refs' => $refs,
);
}
public function getConduitSearchAttachments() {
return array(
id(new DifferentialCommitsSearchEngineAttachment())
->setAttachmentKey('commits'),
);
}
}
diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php
index 3397f9cb0..a2a058568 100644
--- a/src/applications/differential/storage/DifferentialRevision.php
+++ b/src/applications/differential/storage/DifferentialRevision.php
@@ -1,1148 +1,1183 @@
<?php
final class DifferentialRevision extends DifferentialDAO
implements
PhabricatorTokenReceiverInterface,
PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface,
PhabricatorFlaggableInterface,
PhrequentTrackableInterface,
HarbormasterBuildableInterface,
PhabricatorSubscribableInterface,
PhabricatorCustomFieldInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorTimelineInterface,
PhabricatorMentionableInterface,
PhabricatorDestructibleInterface,
PhabricatorProjectInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorConduitResultInterface,
PhabricatorDraftInterface {
protected $title = '';
protected $status;
protected $summary = '';
protected $testPlan = '';
protected $authorPHID;
protected $lastReviewerPHID;
protected $lineCount = 0;
protected $attached = array();
protected $mailKey;
protected $branchName;
protected $repositoryPHID;
protected $activeDiffPHID;
protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
protected $editPolicy = PhabricatorPolicies::POLICY_USER;
protected $properties = array();
private $commits = self::ATTACHABLE;
private $activeDiff = self::ATTACHABLE;
private $diffIDs = self::ATTACHABLE;
private $hashes = self::ATTACHABLE;
private $repository = self::ATTACHABLE;
private $reviewerStatus = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $drafts = array();
private $flags = array();
private $forceMap = array();
const TABLE_COMMIT = 'differential_commit';
const RELATION_REVIEWER = 'revw';
const RELATION_SUBSCRIBED = 'subd';
const PROPERTY_CLOSED_FROM_ACCEPTED = 'wasAcceptedBeforeClose';
const PROPERTY_DRAFT_HOLD = 'draft.hold';
const PROPERTY_SHOULD_BROADCAST = 'draft.broadcast';
const PROPERTY_LINES_ADDED = 'lines.added';
const PROPERTY_LINES_REMOVED = 'lines.removed';
const PROPERTY_BUILDABLES = 'buildables';
public static function initializeNewRevision(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorDifferentialApplication'))
->executeOne();
$view_policy = $app->getPolicy(
DifferentialDefaultViewCapability::CAPABILITY);
if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) {
$initial_state = DifferentialRevisionStatus::DRAFT;
$should_broadcast = false;
} else {
$initial_state = DifferentialRevisionStatus::NEEDS_REVIEW;
$should_broadcast = true;
}
return id(new DifferentialRevision())
->setViewPolicy($view_policy)
->setAuthorPHID($actor->getPHID())
->attachRepository(null)
->attachActiveDiff(null)
->attachReviewers(array())
->setModernRevisionStatus($initial_state)
->setShouldBroadcast($should_broadcast);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'attached' => self::SERIALIZATION_JSON,
'unsubscribed' => self::SERIALIZATION_JSON,
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'title' => 'text255',
'status' => 'text32',
'summary' => 'text',
'testPlan' => 'text',
'authorPHID' => 'phid?',
'lastReviewerPHID' => 'phid?',
'lineCount' => 'uint32?',
'mailKey' => 'bytes40',
'branchName' => 'text255?',
'repositoryPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'authorPHID' => array(
'columns' => array('authorPHID', 'status'),
),
'repositoryPHID' => array(
'columns' => array('repositoryPHID'),
),
// If you (or a project you are a member of) is reviewing a significant
// fraction of the revisions on an install, the result set of open
// revisions may be smaller than the result set of revisions where you
// are a reviewer. In these cases, this key is better than keys on the
// edge table.
'key_status' => array(
'columns' => array('status', 'phid'),
),
),
) + parent::getConfiguration();
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function hasRevisionProperty($key) {
return array_key_exists($key, $this->properties);
}
public function getMonogram() {
$id = $this->getID();
return "D{$id}";
}
public function getURI() {
return '/'.$this->getMonogram();
}
public function loadIDsByCommitPHIDs($phids) {
if (!$phids) {
return array();
}
$revision_ids = queryfx_all(
$this->establishConnection('r'),
'SELECT * FROM %T WHERE commitPHID IN (%Ls)',
self::TABLE_COMMIT,
$phids);
return ipull($revision_ids, 'revisionID', 'commitPHID');
}
public function loadCommitPHIDs() {
if (!$this->getID()) {
return ($this->commits = array());
}
$commits = queryfx_all(
$this->establishConnection('r'),
'SELECT commitPHID FROM %T WHERE revisionID = %d',
self::TABLE_COMMIT,
$this->getID());
$commits = ipull($commits, 'commitPHID');
return ($this->commits = $commits);
}
public function getCommitPHIDs() {
return $this->assertAttached($this->commits);
}
public function getActiveDiff() {
// TODO: Because it's currently technically possible to create a revision
// without an associated diff, we allow an attached-but-null active diff.
// It would be good to get rid of this once we make diff-attaching
// transactional.
return $this->assertAttached($this->activeDiff);
}
public function attachActiveDiff($diff) {
$this->activeDiff = $diff;
return $this;
}
public function getDiffIDs() {
return $this->assertAttached($this->diffIDs);
}
public function attachDiffIDs(array $ids) {
rsort($ids);
$this->diffIDs = array_values($ids);
return $this;
}
public function attachCommitPHIDs(array $phids) {
$this->commits = array_values($phids);
return $this;
}
public function getAttachedPHIDs($type) {
return array_keys(idx($this->attached, $type, array()));
}
public function setAttachedPHIDs($type, array $phids) {
$this->attached[$type] = array_fill_keys($phids, array());
return $this;
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
DifferentialRevisionPHIDType::TYPECONST);
}
public function loadActiveDiff() {
return id(new DifferentialDiff())->loadOneWhere(
'revisionID = %d ORDER BY id DESC LIMIT 1',
$this->getID());
}
public function save() {
if (!$this->getMailKey()) {
$this->mailKey = Filesystem::readRandomCharacters(40);
}
return parent::save();
}
public function getHashes() {
return $this->assertAttached($this->hashes);
}
public function attachHashes(array $hashes) {
$this->hashes = $hashes;
return $this;
}
public function canReviewerForceAccept(
PhabricatorUser $viewer,
DifferentialReviewer $reviewer) {
if (!$reviewer->isPackage()) {
return false;
}
$map = $this->getReviewerForceAcceptMap($viewer);
if (!$map) {
return false;
}
if (isset($map[$reviewer->getReviewerPHID()])) {
return true;
}
return false;
}
private function getReviewerForceAcceptMap(PhabricatorUser $viewer) {
$fragment = $viewer->getCacheFragment();
if (!array_key_exists($fragment, $this->forceMap)) {
$map = $this->newReviewerForceAcceptMap($viewer);
$this->forceMap[$fragment] = $map;
}
return $this->forceMap[$fragment];
}
private function newReviewerForceAcceptMap(PhabricatorUser $viewer) {
$diff = $this->getActiveDiff();
if (!$diff) {
return null;
}
$repository_phid = $diff->getRepositoryPHID();
if (!$repository_phid) {
return null;
}
$paths = array();
try {
$changesets = $diff->getChangesets();
} catch (Exception $ex) {
$changesets = id(new DifferentialChangesetQuery())
->setViewer($viewer)
->withDiffs(array($diff))
->execute();
}
foreach ($changesets as $changeset) {
$paths[] = $changeset->getOwnersFilename();
}
if (!$paths) {
return null;
}
$reviewer_phids = array();
foreach ($this->getReviewers() as $reviewer) {
if (!$reviewer->isPackage()) {
continue;
}
$reviewer_phids[] = $reviewer->getReviewerPHID();
}
if (!$reviewer_phids) {
return null;
}
// Load all the reviewing packages which have control over some of the
// paths in the change. These are packages which the actor may be able
// to force-accept on behalf of.
$control_query = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
->withPHIDs($reviewer_phids)
->withControl($repository_phid, $paths);
$control_packages = $control_query->execute();
if (!$control_packages) {
return null;
}
// Load all the packages which have potential control over some of the
// paths in the change and are owned by the actor. These are packages
// which the actor may be able to use their authority over to gain the
// ability to force-accept for other packages. This query doesn't apply
// dominion rules yet, and we'll bypass those rules later on.
$authority_query = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
->withAuthorityPHIDs(array($viewer->getPHID()))
->withControl($repository_phid, $paths);
$authority_packages = $authority_query->execute();
if (!$authority_packages) {
return null;
}
$authority_packages = mpull($authority_packages, null, 'getPHID');
// Build a map from each path in the revision to the reviewer packages
// which control it.
$control_map = array();
foreach ($paths as $path) {
$control_packages = $control_query->getControllingPackagesForPath(
$repository_phid,
$path);
// Remove packages which the viewer has authority over. We don't need
// to check these for force-accept because they can just accept them
// normally.
$control_packages = mpull($control_packages, null, 'getPHID');
foreach ($control_packages as $phid => $control_package) {
if (isset($authority_packages[$phid])) {
unset($control_packages[$phid]);
}
}
if (!$control_packages) {
continue;
}
$control_map[$path] = $control_packages;
}
if (!$control_map) {
return null;
}
// From here on out, we only care about paths which we have at least one
// controlling package for.
$paths = array_keys($control_map);
// Now, build a map from each path to the packages which would control it
// if there were no dominion rules.
$authority_map = array();
foreach ($paths as $path) {
$authority_packages = $authority_query->getControllingPackagesForPath(
$repository_phid,
$path,
$ignore_dominion = true);
$authority_map[$path] = mpull($authority_packages, null, 'getPHID');
}
// For each path, find the most general package that the viewer has
// authority over. For example, we'll prefer a package that owns "/" to a
// package that owns "/src/".
$force_map = array();
foreach ($authority_map as $path => $package_map) {
$path_fragments = PhabricatorOwnersPackage::splitPath($path);
$fragment_count = count($path_fragments);
// Find the package that we have authority over which has the most
// general match for this path.
$best_match = null;
$best_package = null;
foreach ($package_map as $package_phid => $package) {
$package_paths = $package->getPathsForRepository($repository_phid);
foreach ($package_paths as $package_path) {
// NOTE: A strength of 0 means "no match". A strength of 1 means
// that we matched "/", so we can not possibly find another stronger
// match.
$strength = $package_path->getPathMatchStrength(
$path_fragments,
$fragment_count);
if (!$strength) {
continue;
}
if ($strength < $best_match || !$best_package) {
$best_match = $strength;
$best_package = $package;
if ($strength == 1) {
break 2;
}
}
}
}
if ($best_package) {
$force_map[$path] = array(
'strength' => $best_match,
'package' => $best_package,
);
}
}
// For each path which the viewer owns a package for, find other packages
// which that authority can be used to force-accept. Once we find a way to
// force-accept a package, we don't need to keep looking.
$has_control = array();
foreach ($force_map as $path => $spec) {
$path_fragments = PhabricatorOwnersPackage::splitPath($path);
$fragment_count = count($path_fragments);
$authority_strength = $spec['strength'];
$control_packages = $control_map[$path];
foreach ($control_packages as $control_phid => $control_package) {
if (isset($has_control[$control_phid])) {
continue;
}
$control_paths = $control_package->getPathsForRepository(
$repository_phid);
foreach ($control_paths as $control_path) {
$strength = $control_path->getPathMatchStrength(
$path_fragments,
$fragment_count);
if (!$strength) {
continue;
}
if ($strength > $authority_strength) {
$authority = $spec['package'];
$has_control[$control_phid] = array(
'authority' => $authority,
'phid' => $authority->getPHID(),
);
break;
}
}
}
}
// Return a map from packages which may be force accepted to the packages
// which permit that forced acceptance.
return ipull($has_control, 'phid');
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
// A revision's author (which effectively means "owner" after we added
// commandeering) can always view and edit it.
$author_phid = $this->getAuthorPHID();
if ($author_phid) {
if ($user->getPHID() == $author_phid) {
return true;
}
}
return false;
}
public function describeAutomaticCapability($capability) {
$description = array(
pht('The owner of a revision can always view and edit it.'),
);
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$description[] = pht(
'If a revision belongs to a repository, other users must be able '.
'to view the repository in order to view the revision.');
break;
}
return $description;
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
$extended = array();
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$repository_phid = $this->getRepositoryPHID();
$repository = $this->getRepository();
// Try to use the object if we have it, since it will save us some
// data fetching later on. In some cases, we might not have it.
$repository_ref = nonempty($repository, $repository_phid);
if ($repository_ref) {
$extended[] = array(
$repository_ref,
PhabricatorPolicyCapability::CAN_VIEW,
);
}
break;
}
return $extended;
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
public function getReviewers() {
return $this->assertAttached($this->reviewerStatus);
}
public function attachReviewers(array $reviewers) {
assert_instances_of($reviewers, 'DifferentialReviewer');
$reviewers = mpull($reviewers, null, 'getReviewerPHID');
$this->reviewerStatus = $reviewers;
return $this;
}
public function hasAttachedReviewers() {
return ($this->reviewerStatus !== self::ATTACHABLE);
}
public function getReviewerPHIDs() {
$reviewers = $this->getReviewers();
return mpull($reviewers, 'getReviewerPHID');
}
public function getReviewerPHIDsForEdit() {
$reviewers = $this->getReviewers();
$status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING;
$value = array();
foreach ($reviewers as $reviewer) {
$phid = $reviewer->getReviewerPHID();
if ($reviewer->getReviewerStatus() == $status_blocking) {
$value[] = 'blocking('.$phid.')';
} else {
$value[] = $phid;
}
}
return $value;
}
public function getRepository() {
return $this->assertAttached($this->repository);
}
public function attachRepository(PhabricatorRepository $repository = null) {
$this->repository = $repository;
return $this;
}
public function setModernRevisionStatus($status) {
return $this->setStatus($status);
}
public function getModernRevisionStatus() {
return $this->getStatus();
}
public function getLegacyRevisionStatus() {
return $this->getStatusObject()->getLegacyKey();
}
public function isClosed() {
return $this->getStatusObject()->isClosedStatus();
}
public function isAbandoned() {
return $this->getStatusObject()->isAbandoned();
}
public function isAccepted() {
return $this->getStatusObject()->isAccepted();
}
public function isNeedsReview() {
return $this->getStatusObject()->isNeedsReview();
}
public function isNeedsRevision() {
return $this->getStatusObject()->isNeedsRevision();
}
public function isChangePlanned() {
return $this->getStatusObject()->isChangePlanned();
}
public function isPublished() {
return $this->getStatusObject()->isPublished();
}
public function isDraft() {
return $this->getStatusObject()->isDraft();
}
public function getStatusIcon() {
return $this->getStatusObject()->getIcon();
}
public function getStatusDisplayName() {
return $this->getStatusObject()->getDisplayName();
}
public function getStatusIconColor() {
return $this->getStatusObject()->getIconColor();
}
public function getStatusTagColor() {
return $this->getStatusObject()->getTagColor();
}
public function getStatusObject() {
$status = $this->getStatus();
return DifferentialRevisionStatus::newForStatus($status);
}
public function getFlag(PhabricatorUser $viewer) {
return $this->assertAttachedKey($this->flags, $viewer->getPHID());
}
public function attachFlag(
PhabricatorUser $viewer,
PhabricatorFlag $flag = null) {
$this->flags[$viewer->getPHID()] = $flag;
return $this;
}
public function getHasDraft(PhabricatorUser $viewer) {
return $this->assertAttachedKey($this->drafts, $viewer->getCacheFragment());
}
public function attachHasDraft(PhabricatorUser $viewer, $has_draft) {
$this->drafts[$viewer->getCacheFragment()] = $has_draft;
return $this;
}
public function getHoldAsDraft() {
return $this->getProperty(self::PROPERTY_DRAFT_HOLD, false);
}
public function setHoldAsDraft($hold) {
return $this->setProperty(self::PROPERTY_DRAFT_HOLD, $hold);
}
public function getShouldBroadcast() {
return $this->getProperty(self::PROPERTY_SHOULD_BROADCAST, true);
}
public function setShouldBroadcast($should_broadcast) {
return $this->setProperty(
self::PROPERTY_SHOULD_BROADCAST,
$should_broadcast);
}
public function setAddedLineCount($count) {
return $this->setProperty(self::PROPERTY_LINES_ADDED, $count);
}
public function getAddedLineCount() {
return $this->getProperty(self::PROPERTY_LINES_ADDED);
}
public function setRemovedLineCount($count) {
return $this->setProperty(self::PROPERTY_LINES_REMOVED, $count);
}
public function getRemovedLineCount() {
return $this->getProperty(self::PROPERTY_LINES_REMOVED);
}
public function hasLineCounts() {
// This data was not populated on older revisions, so it may not be
// present on all revisions.
return isset($this->properties[self::PROPERTY_LINES_ADDED]);
}
public function getRevisionScaleGlyphs() {
$add = $this->getAddedLineCount();
$rem = $this->getRemovedLineCount();
$all = ($add + $rem);
if (!$all) {
return ' ';
}
$map = array(
20 => 2,
50 => 3,
150 => 4,
375 => 5,
1000 => 6,
2500 => 7,
);
$n = 1;
foreach ($map as $size => $count) {
if ($size <= $all) {
$n = $count;
} else {
break;
}
}
$add_n = (int)ceil(($add / $all) * $n);
$rem_n = (int)ceil(($rem / $all) * $n);
while ($add_n + $rem_n > $n) {
if ($add_n > 1) {
$add_n--;
} else {
$rem_n--;
}
}
return
str_repeat('+', $add_n).
str_repeat('-', $rem_n).
str_repeat(' ', (7 - $n));
}
public function getBuildableStatus($phid) {
$buildables = $this->getProperty(self::PROPERTY_BUILDABLES);
if (!is_array($buildables)) {
$buildables = array();
}
$buildable = idx($buildables, $phid);
if (!is_array($buildable)) {
$buildable = array();
}
return idx($buildable, 'status');
}
public function setBuildableStatus($phid, $status) {
$buildables = $this->getProperty(self::PROPERTY_BUILDABLES);
if (!is_array($buildables)) {
$buildables = array();
}
$buildable = idx($buildables, $phid);
if (!is_array($buildable)) {
$buildable = array();
}
$buildable['status'] = $status;
$buildables[$phid] = $buildable;
return $this->setProperty(self::PROPERTY_BUILDABLES, $buildables);
}
public function newBuildableStatus(PhabricatorUser $viewer, $phid) {
// For Differential, we're ignoring autobuilds (local lint and unit)
// when computing build status. Differential only cares about remote
// builds when making publishing and undrafting decisions.
$builds = $this->loadImpactfulBuildsForBuildablePHIDs(
$viewer,
array($phid));
return $this->newBuildableStatusForBuilds($builds);
}
public function newBuildableStatusForBuilds(array $builds) {
// If we have nothing but passing builds, the buildable passes.
if (!$builds) {
return HarbormasterBuildableStatus::STATUS_PASSED;
}
// If we have any completed, non-passing builds, the buildable fails.
foreach ($builds as $build) {
if ($build->isComplete()) {
return HarbormasterBuildableStatus::STATUS_FAILED;
}
}
// Otherwise, we're still waiting for the build to pass or fail.
return null;
}
public function loadImpactfulBuilds(PhabricatorUser $viewer) {
$diff = $this->getActiveDiff();
// NOTE: We can't use `withContainerPHIDs()` here because the container
// update in Harbormaster is not synchronous.
$buildables = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withBuildablePHIDs(array($diff->getPHID()))
->withManualBuildables(false)
->execute();
if (!$buildables) {
return array();
}
return $this->loadImpactfulBuildsForBuildablePHIDs(
$viewer,
mpull($buildables, 'getPHID'));
}
private function loadImpactfulBuildsForBuildablePHIDs(
PhabricatorUser $viewer,
array $phids) {
- return id(new HarbormasterBuildQuery())
+ $builds = id(new HarbormasterBuildQuery())
->setViewer($viewer)
->withBuildablePHIDs($phids)
->withAutobuilds(false)
->withBuildStatuses(
array(
HarbormasterBuildStatus::STATUS_INACTIVE,
HarbormasterBuildStatus::STATUS_PENDING,
HarbormasterBuildStatus::STATUS_BUILDING,
HarbormasterBuildStatus::STATUS_FAILED,
HarbormasterBuildStatus::STATUS_ABORTED,
HarbormasterBuildStatus::STATUS_ERROR,
HarbormasterBuildStatus::STATUS_PAUSED,
HarbormasterBuildStatus::STATUS_DEADLOCKED,
))
->execute();
+
+ // Filter builds based on the "Hold Drafts" behavior of their associated
+ // build plans.
+
+ $hold_drafts = HarbormasterBuildPlanBehavior::BEHAVIOR_DRAFTS;
+ $behavior = HarbormasterBuildPlanBehavior::getBehavior($hold_drafts);
+
+ $key_never = HarbormasterBuildPlanBehavior::DRAFTS_NEVER;
+ $key_building = HarbormasterBuildPlanBehavior::DRAFTS_IF_BUILDING;
+
+ foreach ($builds as $key => $build) {
+ $plan = $build->getBuildPlan();
+ $hold_key = $behavior->getPlanOption($plan)->getKey();
+
+ $hold_never = ($hold_key === $key_never);
+ $hold_building = ($hold_key === $key_building);
+
+ // If the build "Never" holds drafts from promoting, we don't care what
+ // the status is.
+ if ($hold_never) {
+ unset($builds[$key]);
+ continue;
+ }
+
+ // If the build holds drafts from promoting "While Building", we only
+ // care about the status until it completes.
+ if ($hold_building) {
+ if ($build->isComplete()) {
+ unset($builds[$key]);
+ continue;
+ }
+ }
+ }
+
+ return $builds;
}
/* -( HarbormasterBuildableInterface )------------------------------------- */
public function getHarbormasterBuildableDisplayPHID() {
return $this->getHarbormasterContainerPHID();
}
public function getHarbormasterBuildablePHID() {
return $this->loadActiveDiff()->getPHID();
}
public function getHarbormasterContainerPHID() {
return $this->getPHID();
}
public function getBuildVariables() {
return array();
}
public function getAvailableBuildVariables() {
return array();
}
public function newBuildableEngine() {
return new DifferentialBuildableEngine();
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
if ($phid == $this->getAuthorPHID()) {
return true;
}
// TODO: This only happens when adding or removing CCs, and is safe from a
// policy perspective, but the subscription pathway should have some
// opportunity to load this data properly. For now, this is the only case
// where implicit subscription is not an intrinsic property of the object.
if ($this->reviewerStatus == self::ATTACHABLE) {
$reviewers = id(new DifferentialRevisionQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($this->getPHID()))
->needReviewers(true)
->executeOne()
->getReviewers();
} else {
$reviewers = $this->getReviewers();
}
foreach ($reviewers as $reviewer) {
if ($reviewer->getReviewerPHID() !== $phid) {
continue;
}
if ($reviewer->isResigned()) {
continue;
}
return true;
}
return false;
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('differential.fields');
}
public function getCustomFieldBaseClass() {
return 'DifferentialCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new DifferentialTransactionEditor();
}
public function getApplicationTransactionTemplate() {
return new DifferentialTransaction();
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$diffs = id(new DifferentialDiffQuery())
->setViewer($engine->getViewer())
->withRevisionIDs(array($this->getID()))
->execute();
foreach ($diffs as $diff) {
$engine->destroyObject($diff);
}
$conn_w = $this->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
self::TABLE_COMMIT,
$this->getID());
// we have to do paths a little differently as they do not have
// an id or phid column for delete() to act on
$dummy_path = new DifferentialAffectedPath();
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
$dummy_path->getTableName(),
$this->getID());
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new DifferentialRevisionFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new DifferentialRevisionFerretEngine();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('title')
->setType('string')
->setDescription(pht('The revision title.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('authorPHID')
->setType('phid')
->setDescription(pht('Revision author PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('status')
->setType('map<string, wild>')
->setDescription(pht('Information about revision status.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('repositoryPHID')
->setType('phid?')
->setDescription(pht('Revision repository PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('diffPHID')
->setType('phid')
->setDescription(pht('Active diff PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('summary')
->setType('string')
->setDescription(pht('Revision summary.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('testPlan')
->setType('string')
->setDescription(pht('Revision test plan.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('isDraft')
->setType('bool')
->setDescription(
pht(
'True if this revision is in any draft state, and thus not '.
'notifying reviewers and subscribers about changes.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('holdAsDraft')
->setType('bool')
->setDescription(
pht(
'True if this revision is being held as a draft. It will not be '.
'automatically submitted for review even if tests pass.')),
);
}
public function getFieldValuesForConduit() {
$status = $this->getStatusObject();
$status_info = array(
'value' => $status->getKey(),
'name' => $status->getDisplayName(),
'closed' => $status->isClosedStatus(),
'color.ansi' => $status->getANSIColor(),
);
return array(
'title' => $this->getTitle(),
'authorPHID' => $this->getAuthorPHID(),
'status' => $status_info,
'repositoryPHID' => $this->getRepositoryPHID(),
'diffPHID' => $this->getActiveDiffPHID(),
'summary' => $this->getSummary(),
'testPlan' => $this->getTestPlan(),
'isDraft' => !$this->getShouldBroadcast(),
'holdAsDraft' => (bool)$this->getHoldAsDraft(),
);
}
public function getConduitSearchAttachments() {
return array(
id(new DifferentialReviewersSearchEngineAttachment())
->setAttachmentKey('reviewers'),
);
}
/* -( PhabricatorDraftInterface )------------------------------------------ */
public function newDraftEngine() {
return new DifferentialRevisionDraftEngine();
}
/* -( PhabricatorTimelineInterface )--------------------------------------- */
public function newTimelineEngine() {
return new DifferentialRevisionTimelineEngine();
}
}
diff --git a/src/applications/differential/view/DifferentialChangesetDetailView.php b/src/applications/differential/view/DifferentialChangesetDetailView.php
index cb697c2e9..211403c8b 100644
--- a/src/applications/differential/view/DifferentialChangesetDetailView.php
+++ b/src/applications/differential/view/DifferentialChangesetDetailView.php
@@ -1,239 +1,228 @@
<?php
final class DifferentialChangesetDetailView extends AphrontView {
private $changeset;
private $buttons = array();
private $editable;
private $symbolIndex;
private $id;
private $vsChangesetID;
private $renderURI;
- private $whitespace;
private $renderingRef;
private $autoload;
private $loaded;
private $renderer;
public function setAutoload($autoload) {
$this->autoload = $autoload;
return $this;
}
public function getAutoload() {
return $this->autoload;
}
public function setLoaded($loaded) {
$this->loaded = $loaded;
return $this;
}
public function getLoaded() {
return $this->loaded;
}
public function setRenderingRef($rendering_ref) {
$this->renderingRef = $rendering_ref;
return $this;
}
public function getRenderingRef() {
return $this->renderingRef;
}
- public function setWhitespace($whitespace) {
- $this->whitespace = $whitespace;
- return $this;
- }
-
- public function getWhitespace() {
- return $this->whitespace;
- }
-
public function setRenderURI($render_uri) {
$this->renderURI = $render_uri;
return $this;
}
public function getRenderURI() {
return $this->renderURI;
}
public function setChangeset($changeset) {
$this->changeset = $changeset;
return $this;
}
public function addButton($button) {
$this->buttons[] = $button;
return $this;
}
public function setEditable($editable) {
$this->editable = $editable;
return $this;
}
public function setSymbolIndex($symbol_index) {
$this->symbolIndex = $symbol_index;
return $this;
}
public function setRenderer($renderer) {
$this->renderer = $renderer;
return $this;
}
public function getRenderer() {
return $this->renderer;
}
public function getID() {
if (!$this->id) {
$this->id = celerity_generate_unique_node_id();
}
return $this->id;
}
public function setID($id) {
$this->id = $id;
return $this;
}
public function setVsChangesetID($vs_changeset_id) {
$this->vsChangesetID = $vs_changeset_id;
return $this;
}
public function getVsChangesetID() {
return $this->vsChangesetID;
}
public function render() {
$this->requireResource('differential-changeset-view-css');
$this->requireResource('syntax-highlighting-css');
Javelin::initBehavior('phabricator-oncopy', array());
$changeset = $this->changeset;
$class = 'differential-changeset';
if (!$this->editable) {
$class .= ' differential-changeset-immutable';
}
$buttons = null;
if ($this->buttons) {
$buttons = phutil_tag(
'div',
array(
'class' => 'differential-changeset-buttons',
),
$this->buttons);
}
$id = $this->getID();
if ($this->symbolIndex) {
Javelin::initBehavior(
'repository-crossreference',
array(
'container' => $id,
) + $this->symbolIndex);
}
$display_filename = $changeset->getDisplayFilename();
$display_icon = FileTypeIcon::getFileIcon($display_filename);
$icon = id(new PHUIIconView())
->setIcon($display_icon);
$renderer = DifferentialChangesetHTMLRenderer::getHTMLRendererByKey(
$this->getRenderer());
$changeset_id = $this->changeset->getID();
$vs_id = $this->getVsChangesetID();
if (!$vs_id) {
// Showing a changeset normally.
$left_id = $changeset_id;
$right_id = $changeset_id;
} else if ($vs_id == -1) {
// Showing a synthetic "deleted" changeset for a file which was
// removed between changes.
$left_id = $changeset_id;
$right_id = null;
} else {
// Showing a diff-of-diffs.
$left_id = $vs_id;
$right_id = $changeset_id;
}
// In the persistent banner, emphasize the current filename.
$path_part = dirname($display_filename);
$file_part = basename($display_filename);
$display_parts = array();
if (strlen($path_part)) {
$path_part = $path_part.'/';
$display_parts[] = phutil_tag(
'span',
array(
'class' => 'diff-banner-path',
),
$path_part);
}
$display_parts[] = phutil_tag(
'span',
array(
'class' => 'diff-banner-file',
),
$file_part);
return javelin_tag(
'div',
array(
'sigil' => 'differential-changeset',
'meta' => array(
'left' => $left_id,
'right' => $right_id,
'renderURI' => $this->getRenderURI(),
- 'whitespace' => $this->getWhitespace(),
'highlight' => null,
'renderer' => $this->getRenderer(),
'ref' => $this->getRenderingRef(),
'autoload' => $this->getAutoload(),
'loaded' => $this->getLoaded(),
'undoTemplates' => hsprintf('%s', $renderer->renderUndoTemplates()),
'displayPath' => hsprintf('%s', $display_parts),
'path' => $display_filename,
'icon' => $display_icon,
'treeNodeID' => 'tree-node-'.$changeset->getAnchorName(),
),
'class' => $class,
'id' => $id,
),
array(
id(new PhabricatorAnchorView())
->setAnchorName($changeset->getAnchorName())
->setNavigationMarker(true)
->render(),
$buttons,
phutil_tag('h1',
array(
'class' => 'differential-file-icon-header',
),
array(
$icon,
$display_filename,
)),
javelin_tag(
'div',
array(
'class' => 'changeset-view-content',
'sigil' => 'changeset-view-content',
),
$this->renderChildren()),
));
}
}
diff --git a/src/applications/differential/view/DifferentialChangesetListView.php b/src/applications/differential/view/DifferentialChangesetListView.php
index 14de553e5..67568005f 100644
--- a/src/applications/differential/view/DifferentialChangesetListView.php
+++ b/src/applications/differential/view/DifferentialChangesetListView.php
@@ -1,424 +1,435 @@
<?php
final class DifferentialChangesetListView extends AphrontView {
private $changesets = array();
private $visibleChangesets = array();
private $references = array();
private $inlineURI;
private $renderURI = '/differential/changeset/';
- private $whitespace;
private $background;
private $header;
private $isStandalone;
private $standaloneURI;
private $leftRawFileURI;
private $rightRawFileURI;
private $inlineListURI;
private $symbolIndexes = array();
private $repository;
private $branch;
private $diff;
private $vsMap = array();
private $title;
private $parser;
public function setParser(DifferentialChangesetParser $parser) {
$this->parser = $parser;
return $this;
}
public function getParser() {
return $this->parser;
}
public function setTitle($title) {
$this->title = $title;
return $this;
}
private function getTitle() {
return $this->title;
}
public function setBranch($branch) {
$this->branch = $branch;
return $this;
}
private function getBranch() {
return $this->branch;
}
public function setChangesets($changesets) {
$this->changesets = $changesets;
return $this;
}
public function setVisibleChangesets($visible_changesets) {
$this->visibleChangesets = $visible_changesets;
return $this;
}
public function setInlineCommentControllerURI($uri) {
$this->inlineURI = $uri;
return $this;
}
public function setInlineListURI($uri) {
$this->inlineListURI = $uri;
return $this;
}
public function getInlineListURI() {
return $this->inlineListURI;
}
public function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function setDiff(DifferentialDiff $diff) {
$this->diff = $diff;
return $this;
}
public function setRenderingReferences(array $references) {
$this->references = $references;
return $this;
}
public function setSymbolIndexes(array $indexes) {
$this->symbolIndexes = $indexes;
return $this;
}
public function setRenderURI($render_uri) {
$this->renderURI = $render_uri;
return $this;
}
- public function setWhitespace($whitespace) {
- $this->whitespace = $whitespace;
- return $this;
- }
-
public function setVsMap(array $vs_map) {
$this->vsMap = $vs_map;
return $this;
}
public function getVsMap() {
return $this->vsMap;
}
public function setStandaloneURI($uri) {
$this->standaloneURI = $uri;
return $this;
}
public function setRawFileURIs($l, $r) {
$this->leftRawFileURI = $l;
$this->rightRawFileURI = $r;
return $this;
}
public function setIsStandalone($is_standalone) {
$this->isStandalone = $is_standalone;
return $this;
}
public function getIsStandalone() {
return $this->isStandalone;
}
public function setBackground($background) {
$this->background = $background;
return $this;
}
public function setHeader($header) {
$this->header = $header;
return $this;
}
public function render() {
$viewer = $this->getViewer();
$this->requireResource('differential-changeset-view-css');
$changesets = $this->changesets;
$renderer = DifferentialChangesetParser::getDefaultRendererForViewer(
$viewer);
$output = array();
$ids = array();
foreach ($changesets as $key => $changeset) {
$file = $changeset->getFilename();
$ref = $this->references[$key];
$detail = id(new DifferentialChangesetDetailView())
->setUser($viewer);
$uniq_id = 'diff-'.$changeset->getAnchorName();
$detail->setID($uniq_id);
$view_options = $this->renderViewOptionsDropdown(
$detail,
$ref,
$changeset);
$detail->setChangeset($changeset);
$detail->addButton($view_options);
$detail->setSymbolIndex(idx($this->symbolIndexes, $key));
$detail->setVsChangesetID(idx($this->vsMap, $changeset->getID()));
$detail->setEditable(true);
$detail->setRenderingRef($ref);
$detail->setRenderURI($this->renderURI);
- $detail->setWhitespace($this->whitespace);
$detail->setRenderer($renderer);
if ($this->getParser()) {
$detail->appendChild($this->getParser()->renderChangeset());
$detail->setLoaded(true);
} else {
$detail->setAutoload(isset($this->visibleChangesets[$key]));
if (isset($this->visibleChangesets[$key])) {
$load = pht('Loading...');
} else {
$load = javelin_tag(
'a',
array(
'class' => 'button button-grey',
'href' => '#'.$uniq_id,
'sigil' => 'differential-load',
'meta' => array(
'id' => $detail->getID(),
'kill' => true,
),
'mustcapture' => true,
),
pht('Load File'));
}
$detail->appendChild(
phutil_tag(
'div',
array(
'id' => $uniq_id,
),
phutil_tag(
'div',
array('class' => 'differential-loading'),
$load)));
}
$output[] = $detail->render();
$ids[] = $detail->getID();
}
$this->requireResource('aphront-tooltip-css');
$this->initBehavior(
'differential-populate',
array(
'changesetViewIDs' => $ids,
'inlineURI' => $this->inlineURI,
'inlineListURI' => $this->inlineListURI,
'isStandalone' => $this->getIsStandalone(),
'pht' => array(
'Open in Editor' => pht('Open in Editor'),
'Show All Context' => pht('Show All Context'),
'All Context Shown' => pht('All Context Shown'),
"Can't Toggle Unloaded File" => pht("Can't Toggle Unloaded File"),
'Expand File' => pht('Expand File'),
'Collapse File' => pht('Collapse File'),
'Browse in Diffusion' => pht('Browse in Diffusion'),
'View Standalone' => pht('View Standalone'),
'Show Raw File (Left)' => pht('Show Raw File (Left)'),
'Show Raw File (Right)' => pht('Show Raw File (Right)'),
'Configure Editor' => pht('Configure Editor'),
'Load Changes' => pht('Load Changes'),
'View Side-by-Side' => pht('View Side-by-Side'),
'View Unified' => pht('View Unified'),
'Change Text Encoding...' => pht('Change Text Encoding...'),
'Highlight As...' => pht('Highlight As...'),
'Loading...' => pht('Loading...'),
'Editing Comment' => pht('Editing Comment'),
'Jump to next change.' => pht('Jump to next change.'),
'Jump to previous change.' => pht('Jump to previous change.'),
'Jump to next file.' => pht('Jump to next file.'),
'Jump to previous file.' => pht('Jump to previous file.'),
'Jump to next inline comment.' => pht('Jump to next inline comment.'),
'Jump to previous inline comment.' =>
pht('Jump to previous inline comment.'),
'Jump to the table of contents.' =>
pht('Jump to the table of contents.'),
'Edit selected inline comment.' =>
pht('Edit selected inline comment.'),
'You must select a comment to edit.' =>
pht('You must select a comment to edit.'),
'Reply to selected inline comment or change.' =>
pht('Reply to selected inline comment or change.'),
'You must select a comment or change to reply to.' =>
pht('You must select a comment or change to reply to.'),
'Reply and quote selected inline comment.' =>
pht('Reply and quote selected inline comment.'),
'Mark or unmark selected inline comment as done.' =>
pht('Mark or unmark selected inline comment as done.'),
'You must select a comment to mark done.' =>
pht('You must select a comment to mark done.'),
'Collapse or expand inline comment.' =>
pht('Collapse or expand inline comment.'),
'You must select a comment to hide.' =>
pht('You must select a comment to hide.'),
'Jump to next inline comment, including collapsed comments.' =>
pht('Jump to next inline comment, including collapsed comments.'),
'Jump to previous inline comment, including collapsed comments.' =>
pht('Jump to previous inline comment, including collapsed comments.'),
'This file content has been collapsed.' =>
pht('This file content has been collapsed.'),
'Show Content' => pht('Show Content'),
'Hide or show the current file.' =>
pht('Hide or show the current file.'),
'You must select a file to hide or show.' =>
pht('You must select a file to hide or show.'),
'Unsaved' => pht('Unsaved'),
'Unsubmitted' => pht('Unsubmitted'),
'Comments' => pht('Comments'),
'Hide "Done" Inlines' => pht('Hide "Done" Inlines'),
'Hide Collapsed Inlines' => pht('Hide Collapsed Inlines'),
'Hide Older Inlines' => pht('Hide Older Inlines'),
'Hide All Inlines' => pht('Hide All Inlines'),
'Show All Inlines' => pht('Show All Inlines'),
'List Inline Comments' => pht('List Inline Comments'),
'Display Options' => pht('Display Options'),
'Hide or show all inline comments.' =>
pht('Hide or show all inline comments.'),
'Finish editing inline comments before changing display modes.' =>
pht('Finish editing inline comments before changing display modes.'),
),
));
if ($this->header) {
$header = $this->header;
} else {
$header = id(new PHUIHeaderView())
->setHeader($this->getTitle());
}
$content = phutil_tag(
'div',
array(
'class' => 'differential-review-stage',
'id' => 'differential-review-stage',
),
$output);
$object_box = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground($this->background)
->setCollapsed(true)
->appendChild($content);
return $object_box;
}
private function renderViewOptionsDropdown(
DifferentialChangesetDetailView $detail,
$ref,
DifferentialChangeset $changeset) {
$viewer = $this->getViewer();
$meta = array();
$qparams = array(
- 'ref' => $ref,
- 'whitespace' => $this->whitespace,
+ 'ref' => $ref,
);
if ($this->standaloneURI) {
$uri = new PhutilURI($this->standaloneURI);
- $uri->setQueryParams($uri->getQueryParams() + $qparams);
+ $uri = $this->appendDefaultQueryParams($uri, $qparams);
$meta['standaloneURI'] = (string)$uri;
}
$repository = $this->repository;
if ($repository) {
try {
$meta['diffusionURI'] =
(string)$repository->getDiffusionBrowseURIForPath(
$viewer,
$changeset->getAbsoluteRepositoryPath($repository, $this->diff),
idx($changeset->getMetadata(), 'line:first'),
$this->getBranch());
} catch (DiffusionSetupException $e) {
// Ignore
}
}
$change = $changeset->getChangeType();
if ($this->leftRawFileURI) {
if ($change != DifferentialChangeType::TYPE_ADD) {
$uri = new PhutilURI($this->leftRawFileURI);
- $uri->setQueryParams($uri->getQueryParams() + $qparams);
+ $uri = $this->appendDefaultQueryParams($uri, $qparams);
$meta['leftURI'] = (string)$uri;
}
}
if ($this->rightRawFileURI) {
if ($change != DifferentialChangeType::TYPE_DELETE &&
$change != DifferentialChangeType::TYPE_MULTICOPY) {
$uri = new PhutilURI($this->rightRawFileURI);
- $uri->setQueryParams($uri->getQueryParams() + $qparams);
+ $uri = $this->appendDefaultQueryParams($uri, $qparams);
$meta['rightURI'] = (string)$uri;
}
}
if ($viewer && $repository) {
$path = ltrim(
$changeset->getAbsoluteRepositoryPath($repository, $this->diff),
'/');
$line = idx($changeset->getMetadata(), 'line:first', 1);
$editor_link = $viewer->loadEditorLink($path, $line, $repository);
if ($editor_link) {
$meta['editor'] = $editor_link;
} else {
$meta['editorConfigure'] = '/settings/panel/display/';
}
}
$meta['containerID'] = $detail->getID();
return id(new PHUIButtonView())
->setTag('a')
->setText(pht('View Options'))
->setIcon('fa-bars')
->setColor(PHUIButtonView::GREY)
->setHref(idx($meta, 'detailURI', '#'))
->setMetadata($meta)
->addSigil('differential-view-options');
}
+ private function appendDefaultQueryParams(PhutilURI $uri, array $params) {
+ // Add these default query parameters to the query string if they do not
+ // already exist.
+
+ $have = array();
+ foreach ($uri->getQueryParamsAsPairList() as $pair) {
+ list($key, $value) = $pair;
+ $have[$key] = true;
+ }
+
+ foreach ($params as $key => $value) {
+ if (!isset($have[$key])) {
+ $uri->appendQueryParam($key, $value);
+ }
+ }
+
+ return $uri;
+ }
+
}
diff --git a/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php b/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php
index a77b320d8..07ca983bc 100644
--- a/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php
+++ b/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php
@@ -1,439 +1,404 @@
<?php
final class DifferentialRevisionUpdateHistoryView extends AphrontView {
private $diffs = array();
private $selectedVersusDiffID;
private $selectedDiffID;
- private $selectedWhitespace;
private $commitsForLinks = array();
private $unitStatus = array();
public function setDiffs(array $diffs) {
assert_instances_of($diffs, 'DifferentialDiff');
$this->diffs = $diffs;
return $this;
}
public function setSelectedVersusDiffID($id) {
$this->selectedVersusDiffID = $id;
return $this;
}
public function setSelectedDiffID($id) {
$this->selectedDiffID = $id;
return $this;
}
- public function setSelectedWhitespace($whitespace) {
- $this->selectedWhitespace = $whitespace;
- return $this;
- }
-
public function setCommitsForLinks(array $commits) {
assert_instances_of($commits, 'PhabricatorRepositoryCommit');
$this->commitsForLinks = $commits;
return $this;
}
public function setDiffUnitStatuses(array $unit_status) {
$this->unitStatus = $unit_status;
return $this;
}
public function render() {
$this->requireResource('differential-core-view-css');
$this->requireResource('differential-revision-history-css');
$data = array(
array(
'name' => pht('Base'),
'id' => null,
'desc' => pht('Base'),
'age' => null,
'obj' => null,
),
);
$seq = 0;
foreach ($this->diffs as $diff) {
$data[] = array(
'name' => pht('Diff %d', ++$seq),
'id' => $diff->getID(),
'desc' => $diff->getDescription(),
'age' => $diff->getDateCreated(),
'obj' => $diff,
);
}
$max_id = $diff->getID();
$revision_id = $diff->getRevisionID();
$idx = 0;
$rows = array();
$disable = false;
$radios = array();
$last_base = null;
$rowc = array();
foreach ($data as $row) {
$diff = $row['obj'];
$name = $row['name'];
$id = $row['id'];
$old_class = false;
$new_class = false;
if ($id) {
$new_checked = ($this->selectedDiffID == $id);
$new = javelin_tag(
'input',
array(
'type' => 'radio',
'name' => 'id',
'value' => $id,
'checked' => $new_checked ? 'checked' : null,
'sigil' => 'differential-new-radio',
));
if ($new_checked) {
$new_class = true;
$disable = true;
}
$new = phutil_tag(
'div',
array(
'class' => 'differential-update-history-radio',
),
$new);
} else {
$new = null;
}
if ($max_id != $id) {
$uniq = celerity_generate_unique_node_id();
$old_checked = ($this->selectedVersusDiffID == $id);
$old = phutil_tag(
'input',
array(
'type' => 'radio',
'name' => 'vs',
'value' => $id,
'id' => $uniq,
'checked' => $old_checked ? 'checked' : null,
'disabled' => $disable ? 'disabled' : null,
));
$radios[] = $uniq;
if ($old_checked) {
$old_class = true;
}
$old = phutil_tag(
'div',
array(
'class' => 'differential-update-history-radio',
),
$old);
} else {
$old = null;
}
$desc = $row['desc'];
if ($row['age']) {
$age = phabricator_datetime($row['age'], $this->getUser());
} else {
$age = null;
}
if ($diff) {
$unit_status = idx(
$this->unitStatus,
$diff->getPHID(),
$diff->getUnitStatus());
$lint = self::renderDiffLintStar($row['obj']);
$lint = phutil_tag(
'div',
array(
'class' => 'lintunit-star',
'title' => self::getDiffLintMessage($diff),
),
$lint);
$unit = self::renderDiffUnitStar($unit_status);
$unit = phutil_tag(
'div',
array(
'class' => 'lintunit-star',
'title' => self::getDiffUnitMessage($unit_status),
),
$unit);
$base = $this->renderBaseRevision($diff);
} else {
$lint = null;
$unit = null;
$base = null;
}
if ($last_base !== null && $base !== $last_base) {
// TODO: Render some kind of notice about rebases.
}
$last_base = $base;
if ($revision_id) {
$id_link = phutil_tag(
'a',
array(
'href' => '/D'.$revision_id.'?id='.$id,
),
$id);
} else {
$id_link = phutil_tag(
'a',
array(
'href' => '/differential/diff/'.$id.'/',
),
$id);
}
$rows[] = array(
$name,
$id_link,
$base,
$desc,
$age,
$lint,
$unit,
$old,
$new,
);
$classes = array();
if ($old_class) {
$classes[] = 'differential-update-history-old-now';
}
if ($new_class) {
$classes[] = 'differential-update-history-new-now';
}
$rowc[] = nonempty(implode(' ', $classes), null);
}
Javelin::initBehavior(
'differential-diff-radios',
array(
'radios' => $radios,
));
- $options = array(
- DifferentialChangesetParser::WHITESPACE_IGNORE_ALL => pht('Ignore All'),
- DifferentialChangesetParser::WHITESPACE_IGNORE_MOST => pht('Ignore Most'),
- DifferentialChangesetParser::WHITESPACE_IGNORE_TRAILING =>
- pht('Ignore Trailing'),
- DifferentialChangesetParser::WHITESPACE_SHOW_ALL => pht('Show All'),
- );
-
- foreach ($options as $value => $label) {
- $options[$value] = phutil_tag(
- 'option',
- array(
- 'value' => $value,
- 'selected' => ($value == $this->selectedWhitespace)
- ? 'selected'
- : null,
- ),
- $label);
- }
- $select = phutil_tag('select', array('name' => 'whitespace'), $options);
-
-
$table = id(new AphrontTableView($rows));
$table->setHeaders(
array(
pht('Diff'),
pht('ID'),
pht('Base'),
pht('Description'),
pht('Created'),
pht('Lint'),
pht('Unit'),
'',
'',
));
$table->setColumnClasses(
array(
'pri',
'',
'',
'wide',
'date',
'center',
'center',
'center differential-update-history-old',
'center differential-update-history-new',
));
$table->setRowClasses($rowc);
$table->setDeviceVisibility(
array(
true,
true,
false,
true,
false,
false,
false,
true,
true,
));
$show_diff = phutil_tag(
'div',
array(
'class' => 'differential-update-history-footer',
),
array(
- phutil_tag(
- 'label',
- array(),
- array(
- pht('Whitespace Changes:'),
- $select,
- )),
phutil_tag(
'button',
array(),
pht('Show Diff')),
));
$content = phabricator_form(
$this->getUser(),
array(
'action' => '/D'.$revision_id.'#toc',
),
array(
$table,
$show_diff,
));
return $content;
}
const STAR_NONE = 'none';
const STAR_OKAY = 'okay';
const STAR_WARN = 'warn';
const STAR_FAIL = 'fail';
const STAR_SKIP = 'skip';
private static function renderDiffLintStar(DifferentialDiff $diff) {
static $map = array(
DifferentialLintStatus::LINT_NONE => self::STAR_NONE,
DifferentialLintStatus::LINT_OKAY => self::STAR_OKAY,
DifferentialLintStatus::LINT_WARN => self::STAR_WARN,
DifferentialLintStatus::LINT_FAIL => self::STAR_FAIL,
DifferentialLintStatus::LINT_SKIP => self::STAR_SKIP,
DifferentialLintStatus::LINT_AUTO_SKIP => self::STAR_SKIP,
);
$star = idx($map, $diff->getLintStatus(), self::STAR_FAIL);
return self::renderDiffStar($star);
}
private static function renderDiffUnitStar($unit_status) {
static $map = array(
DifferentialUnitStatus::UNIT_NONE => self::STAR_NONE,
DifferentialUnitStatus::UNIT_OKAY => self::STAR_OKAY,
DifferentialUnitStatus::UNIT_WARN => self::STAR_WARN,
DifferentialUnitStatus::UNIT_FAIL => self::STAR_FAIL,
DifferentialUnitStatus::UNIT_SKIP => self::STAR_SKIP,
DifferentialUnitStatus::UNIT_AUTO_SKIP => self::STAR_SKIP,
);
$star = idx($map, $unit_status, self::STAR_FAIL);
return self::renderDiffStar($star);
}
public static function getDiffLintMessage(DifferentialDiff $diff) {
switch ($diff->getLintStatus()) {
case DifferentialLintStatus::LINT_NONE:
return pht('No Linters Available');
case DifferentialLintStatus::LINT_OKAY:
return pht('Lint OK');
case DifferentialLintStatus::LINT_WARN:
return pht('Lint Warnings');
case DifferentialLintStatus::LINT_FAIL:
return pht('Lint Errors');
case DifferentialLintStatus::LINT_SKIP:
return pht('Lint Skipped');
case DifferentialLintStatus::LINT_AUTO_SKIP:
return pht('Automatic diff as part of commit; lint not applicable.');
}
return pht('Unknown');
}
public static function getDiffUnitMessage($unit_status) {
switch ($unit_status) {
case DifferentialUnitStatus::UNIT_NONE:
return pht('No Unit Test Coverage');
case DifferentialUnitStatus::UNIT_OKAY:
return pht('Unit Tests OK');
case DifferentialUnitStatus::UNIT_WARN:
return pht('Unit Test Warnings');
case DifferentialUnitStatus::UNIT_FAIL:
return pht('Unit Test Errors');
case DifferentialUnitStatus::UNIT_SKIP:
return pht('Unit Tests Skipped');
case DifferentialUnitStatus::UNIT_AUTO_SKIP:
return pht(
'Automatic diff as part of commit; unit tests not applicable.');
}
return pht('Unknown');
}
private static function renderDiffStar($star) {
$class = 'diff-star-'.$star;
return phutil_tag(
'span',
array('class' => $class),
"\xE2\x98\x85");
}
private function renderBaseRevision(DifferentialDiff $diff) {
switch ($diff->getSourceControlSystem()) {
case 'git':
$base = $diff->getSourceControlBaseRevision();
if (strpos($base, '@') === false) {
$label = substr($base, 0, 7);
} else {
// The diff is from git-svn
$base = explode('@', $base);
$base = last($base);
$label = $base;
}
break;
case 'svn':
$base = $diff->getSourceControlBaseRevision();
$base = explode('@', $base);
$base = last($base);
$label = $base;
break;
default:
$label = null;
break;
}
$link = null;
if ($label) {
$commit_for_link = idx(
$this->commitsForLinks,
$diff->getSourceControlBaseRevision());
if ($commit_for_link) {
$link = phutil_tag(
'a',
array('href' => $commit_for_link->getURI()),
$label);
} else {
$link = $label;
}
}
return $link;
}
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionWrongBuildsTransaction.php b/src/applications/differential/xaction/DifferentialRevisionWrongBuildsTransaction.php
new file mode 100644
index 000000000..260813b75
--- /dev/null
+++ b/src/applications/differential/xaction/DifferentialRevisionWrongBuildsTransaction.php
@@ -0,0 +1,37 @@
+<?php
+
+final class DifferentialRevisionWrongBuildsTransaction
+ extends DifferentialRevisionTransactionType {
+
+ const TRANSACTIONTYPE = 'differential.builds.wrong';
+
+ public function generateOldValue($object) {
+ return null;
+ }
+
+ public function generateNewValue($object, $value) {
+ return $value;
+ }
+
+ public function getIcon() {
+ return 'fa-exclamation';
+ }
+
+ public function getColor() {
+ return 'pink';
+ }
+
+ public function getActionStrength() {
+ return 4;
+ }
+
+ public function getTitle() {
+ return pht(
+ 'This revision was landed with ongoing or failed builds.');
+ }
+
+ public function shouldHideForFeed() {
+ return true;
+ }
+
+}
diff --git a/src/applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php
index fe99471b0..0b6ae19e3 100644
--- a/src/applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php
@@ -1,546 +1,548 @@
<?php
final class DiffusionBrowseQueryConduitAPIMethod
extends DiffusionQueryConduitAPIMethod {
public function getAPIMethodName() {
return 'diffusion.browsequery';
}
public function getMethodDescription() {
return pht(
'File(s) information for a repository at an (optional) path and '.
'(optional) commit.');
}
protected function defineReturnType() {
return 'array';
}
protected function defineCustomParamTypes() {
return array(
'path' => 'optional string',
'commit' => 'optional string',
'needValidityOnly' => 'optional bool',
'limit' => 'optional int',
'offset' => 'optional int',
);
}
protected function getResult(ConduitAPIRequest $request) {
$result = parent::getResult($request);
return $result->toDictionary();
}
protected function getGitResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$path = $request->getValue('path');
$commit = $request->getValue('commit');
$offset = (int)$request->getValue('offset');
$limit = (int)$request->getValue('limit');
$result = $this->getEmptyResultSet();
if ($path == '') {
// Fast path to improve the performance of the repository view; we know
// the root is always a tree at any commit and always exists.
$stdout = 'tree';
} else {
try {
list($stdout) = $repository->execxLocalCommand(
'cat-file -t %s:%s',
$commit,
$path);
} catch (CommandException $e) {
// The "cat-file" command may fail if the path legitimately does not
// exist, but it may also fail if the path is a submodule. This can
// produce either "Not a valid object name" or "could not get object
// info".
// To detect if we have a submodule, use `git ls-tree`. If the path
// is a submodule, we'll get a "160000" mode mask with type "commit".
list($sub_err, $sub_stdout) = $repository->execLocalCommand(
'ls-tree %s -- %s',
$commit,
$path);
if (!$sub_err) {
// If the path failed "cat-file" but "ls-tree" worked, we assume it
// must be a submodule. If it is, the output will look something
// like this:
//
// 160000 commit <hash> <path>
//
// We make sure it has the 160000 mode mask to confirm that it's
// definitely a submodule.
$mode = (int)$sub_stdout;
if ($mode & 160000) {
$submodule_reason = DiffusionBrowseResultSet::REASON_IS_SUBMODULE;
$result
->setReasonForEmptyResultSet($submodule_reason);
return $result;
}
}
$stderr = $e->getStderr();
if (preg_match('/^fatal: Not a valid object name/', $stderr)) {
// Grab two logs, since the first one is when the object was deleted.
list($stdout) = $repository->execxLocalCommand(
'log -n2 --format="%%H" %s -- %s',
$commit,
$path);
$stdout = trim($stdout);
if ($stdout) {
$commits = explode("\n", $stdout);
$result
->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_DELETED)
->setDeletedAtCommit(idx($commits, 0))
->setExistedAtCommit(idx($commits, 1));
return $result;
}
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);
return $result;
} else {
throw $e;
}
}
}
if (trim($stdout) == 'blob') {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_FILE);
return $result;
}
$result->setIsValidResults(true);
if ($this->shouldOnlyTestValidity($request)) {
return $result;
}
list($stdout) = $repository->execxLocalCommand(
'ls-tree -z -l %s:%s',
$commit,
$path);
$submodules = array();
if (strlen($path)) {
$prefix = rtrim($path, '/').'/';
} else {
$prefix = '';
}
$count = 0;
$results = array();
$lines = empty($stdout)
? array()
: explode("\0", rtrim($stdout));
foreach ($lines as $line) {
// NOTE: Limit to 5 components so we parse filenames with spaces in them
// correctly.
// NOTE: The output uses a mixture of tabs and one-or-more spaces to
// delimit fields.
$parts = preg_split('/\s+/', $line, 5);
if (count($parts) < 5) {
throw new Exception(
pht(
'Expected "<mode> <type> <hash> <size>\t<name>", for ls-tree of '.
'"%s:%s", got: %s',
$commit,
$path,
$line));
}
list($mode, $type, $hash, $size, $name) = $parts;
$path_result = new DiffusionRepositoryPath();
if ($type == 'tree') {
$file_type = DifferentialChangeType::FILE_DIRECTORY;
} else if ($type == 'commit') {
$file_type = DifferentialChangeType::FILE_SUBMODULE;
$submodules[] = $path_result;
} else {
$mode = intval($mode, 8);
if (($mode & 0120000) == 0120000) {
$file_type = DifferentialChangeType::FILE_SYMLINK;
} else {
$file_type = DifferentialChangeType::FILE_NORMAL;
}
}
$path_result->setFullPath($prefix.$name);
$path_result->setPath($name);
$path_result->setHash($hash);
$path_result->setFileType($file_type);
$path_result->setFileSize($size);
if ($count >= $offset) {
$results[] = $path_result;
}
$count++;
if ($limit && $count >= ($offset + $limit)) {
break;
}
}
// If we identified submodules, lookup the module info at this commit to
// find their source URIs.
if ($submodules) {
// NOTE: We need to read the file out of git and write it to a temporary
// location because "git config -f" doesn't accept a "commit:path"-style
// argument.
// NOTE: This file may not exist, e.g. because the commit author removed
// it when they added the submodule. See T1448. If it's not present, just
// show the submodule without enriching it. If ".gitmodules" was removed
// it seems to partially break submodules, but the repository as a whole
// continues to work fine and we've seen at least two cases of this in
// the wild.
list($err, $contents) = $repository->execLocalCommand(
'cat-file blob %s:.gitmodules',
$commit);
if (!$err) {
$tmp = new TempFile();
Filesystem::writeFile($tmp, $contents);
list($module_info) = $repository->execxLocalCommand(
'config -l -f %s',
$tmp);
$dict = array();
$lines = explode("\n", trim($module_info));
foreach ($lines as $line) {
list($key, $value) = explode('=', $line, 2);
$parts = explode('.', $key);
$dict[$key] = $value;
}
foreach ($submodules as $path) {
$full_path = $path->getFullPath();
$key = 'submodule.'.$full_path.'.url';
if (isset($dict[$key])) {
$path->setExternalURI($dict[$key]);
}
}
}
}
return $result->setPaths($results);
}
protected function getMercurialResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$path = $request->getValue('path');
$commit = $request->getValue('commit');
$offset = (int)$request->getValue('offset');
$limit = (int)$request->getValue('limit');
$result = $this->getEmptyResultSet();
$entire_manifest = id(new DiffusionLowLevelMercurialPathsQuery())
->setRepository($repository)
->withCommit($commit)
->withPath($path)
->execute();
$results = array();
$match_against = trim($path, '/');
$match_len = strlen($match_against);
// For the root, don't trim. For other paths, trim the "/" after we match.
// We need this because Mercurial's canonical paths have no leading "/",
// but ours do.
$trim_len = $match_len ? $match_len + 1 : 0;
$count = 0;
foreach ($entire_manifest as $path) {
if (strncmp($path, $match_against, $match_len)) {
continue;
}
if (!strlen($path)) {
continue;
}
$remainder = substr($path, $trim_len);
if (!strlen($remainder)) {
// There is a file with this exact name in the manifest, so clearly
// it's a file.
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_FILE);
return $result;
}
$parts = explode('/', $remainder);
$name = reset($parts);
// If we've already seen this path component, we're looking at a file
// inside a directory we already processed. Just move on.
if (isset($results[$name])) {
continue;
}
if (count($parts) == 1) {
$type = DifferentialChangeType::FILE_NORMAL;
} else {
$type = DifferentialChangeType::FILE_DIRECTORY;
}
if ($count >= $offset) {
$results[$name] = $type;
}
$count++;
if ($limit && ($count >= ($offset + $limit))) {
break;
}
}
foreach ($results as $key => $type) {
$path_result = new DiffusionRepositoryPath();
$path_result->setPath($key);
$path_result->setFileType($type);
$path_result->setFullPath(ltrim($match_against.'/', '/').$key);
$results[$key] = $path_result;
}
$valid_results = true;
if (empty($results)) {
// TODO: Detect "deleted" by issuing "hg log"?
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);
$valid_results = false;
}
return $result
->setPaths($results)
->setIsValidResults($valid_results);
}
protected function getSVNResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$path = $request->getValue('path');
$commit = $request->getValue('commit');
$offset = (int)$request->getValue('offset');
$limit = (int)$request->getValue('limit');
$result = $this->getEmptyResultSet();
$subpath = $repository->getDetail('svn-subpath');
if ($subpath && strncmp($subpath, $path, strlen($subpath))) {
// If we have a subpath and the path isn't a child of it, it (almost
// certainly) won't exist since we don't track commits which affect
// it. (Even if it exists, return a consistent result.)
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_UNTRACKED_PARENT);
return $result;
}
$conn_r = $repository->establishConnection('r');
$parent_path = DiffusionPathIDQuery::getParentPath($path);
$path_query = new DiffusionPathIDQuery(
array(
$path,
$parent_path,
));
$path_map = $path_query->loadPathIDs();
$path_id = $path_map[$path];
$parent_path_id = $path_map[$parent_path];
if (empty($path_id)) {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);
return $result;
}
if ($commit) {
- $slice_clause = 'AND svnCommit <= '.(int)$commit;
+ $slice_clause = qsprintf($conn_r, 'AND svnCommit <= %d', $commit);
} else {
- $slice_clause = '';
+ $slice_clause = qsprintf($conn_r, '');
}
$index = queryfx_all(
$conn_r,
'SELECT pathID, max(svnCommit) maxCommit FROM %T WHERE
repositoryID = %d AND parentID = %d
%Q GROUP BY pathID',
PhabricatorRepository::TABLE_FILESYSTEM,
$repository->getID(),
$path_id,
$slice_clause);
if (!$index) {
if ($path == '/') {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_EMPTY);
} else {
// NOTE: The parent path ID is included so this query can take
// advantage of the table's primary key; it is uniquely determined by
// the pathID but if we don't do the lookup ourselves MySQL doesn't have
// the information it needs to avoid a table scan.
$reasons = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE repositoryID = %d
AND parentID = %d
AND pathID = %d
%Q ORDER BY svnCommit DESC LIMIT 2',
PhabricatorRepository::TABLE_FILESYSTEM,
$repository->getID(),
$parent_path_id,
$path_id,
$slice_clause);
$reason = reset($reasons);
if (!$reason) {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);
} else {
$file_type = $reason['fileType'];
if (empty($reason['existed'])) {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_DELETED);
$result->setDeletedAtCommit($reason['svnCommit']);
if (!empty($reasons[1])) {
$result->setExistedAtCommit($reasons[1]['svnCommit']);
}
} else if ($file_type == DifferentialChangeType::FILE_DIRECTORY) {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_EMPTY);
} else {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_FILE);
}
}
}
return $result;
}
$result->setIsValidResults(true);
if ($this->shouldOnlyTestValidity($request)) {
return $result;
}
$sql = array();
foreach ($index as $row) {
- $sql[] =
- '(pathID = '.(int)$row['pathID'].' AND '.
- 'svnCommit = '.(int)$row['maxCommit'].')';
+ $sql[] = qsprintf(
+ $conn_r,
+ '(pathID = %d AND svnCommit = %d)',
+ $row['pathID'],
+ $row['maxCommit']);
}
$browse = queryfx_all(
$conn_r,
'SELECT *, p.path pathName
FROM %T f JOIN %T p ON f.pathID = p.id
WHERE repositoryID = %d
AND parentID = %d
AND existed = 1
AND (%LO)
ORDER BY pathName',
PhabricatorRepository::TABLE_FILESYSTEM,
PhabricatorRepository::TABLE_PATH,
$repository->getID(),
$path_id,
$sql);
$loadable_commits = array();
foreach ($browse as $key => $file) {
// We need to strip out directories because we don't store last-modified
// in the filesystem table.
if ($file['fileType'] != DifferentialChangeType::FILE_DIRECTORY) {
$loadable_commits[] = $file['svnCommit'];
$browse[$key]['hasCommit'] = true;
}
}
$commits = array();
$commit_data = array();
if ($loadable_commits) {
// NOTE: Even though these are integers, use '%Ls' because MySQL doesn't
// use the second part of the key otherwise!
$commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(
'repositoryID = %d AND commitIdentifier IN (%Ls)',
$repository->getID(),
$loadable_commits);
$commits = mpull($commits, null, 'getCommitIdentifier');
if ($commits) {
$commit_data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(
'commitID in (%Ld)',
mpull($commits, 'getID'));
$commit_data = mpull($commit_data, null, 'getCommitID');
} else {
$commit_data = array();
}
}
$path_normal = DiffusionPathIDQuery::normalizePath($path);
$results = array();
$count = 0;
foreach ($browse as $file) {
$full_path = $file['pathName'];
$file_path = ltrim(substr($full_path, strlen($path_normal)), '/');
$full_path = ltrim($full_path, '/');
$result_path = new DiffusionRepositoryPath();
$result_path->setPath($file_path);
$result_path->setFullPath($full_path);
$result_path->setFileType($file['fileType']);
if (!empty($file['hasCommit'])) {
$commit = idx($commits, $file['svnCommit']);
if ($commit) {
$data = idx($commit_data, $commit->getID());
$result_path->setLastModifiedCommit($commit);
$result_path->setLastCommitData($data);
}
}
if ($count >= $offset) {
$results[] = $result_path;
}
$count++;
if ($limit && ($count >= ($offset + $limit))) {
break;
}
}
if (empty($results)) {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_EMPTY);
}
return $result->setPaths($results);
}
private function getEmptyResultSet() {
return id(new DiffusionBrowseResultSet())
->setPaths(array())
->setReasonForEmptyResultSet(null)
->setIsValidResults(false);
}
private function shouldOnlyTestValidity(ConduitAPIRequest $request) {
return $request->getValue('needValidityOnly', false);
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php
index 4c1d39e8c..ebce21dd6 100644
--- a/src/applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php
@@ -1,282 +1,286 @@
<?php
final class DiffusionHistoryQueryConduitAPIMethod
extends DiffusionQueryConduitAPIMethod {
private $parents = array();
public function getAPIMethodName() {
return 'diffusion.historyquery';
}
public function getMethodDescription() {
return pht(
'Returns history information for a repository at a specific '.
'commit and path.');
}
protected function defineReturnType() {
return 'array';
}
protected function defineCustomParamTypes() {
return array(
'commit' => 'required string',
'against' => 'optional string',
'path' => 'required string',
'offset' => 'required int',
'limit' => 'required int',
'needDirectChanges' => 'optional bool',
'needChildChanges' => 'optional bool',
);
}
protected function getResult(ConduitAPIRequest $request) {
$path_changes = parent::getResult($request);
return array(
'pathChanges' => mpull($path_changes, 'toDictionary'),
'parents' => $this->parents,
);
}
protected function getGitResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$commit_hash = $request->getValue('commit');
$against_hash = $request->getValue('against');
$path = $request->getValue('path');
$offset = $request->getValue('offset');
$limit = $request->getValue('limit');
if (strlen($against_hash)) {
$commit_range = "${against_hash}..${commit_hash}";
} else {
$commit_range = $commit_hash;
}
list($stdout) = $repository->execxLocalCommand(
'log '.
'--skip=%d '.
'-n %d '.
'--pretty=format:%s '.
'%s -- %C',
$offset,
$limit,
'%H:%P',
$commit_range,
// Git omits merge commits if the path is provided, even if it is empty.
(strlen($path) ? csprintf('%s', $path) : ''));
$lines = explode("\n", trim($stdout));
$lines = array_filter($lines);
$hash_list = array();
$parent_map = array();
foreach ($lines as $line) {
list($hash, $parents) = explode(':', $line);
$hash_list[] = $hash;
$parent_map[$hash] = preg_split('/\s+/', $parents);
}
$this->parents = $parent_map;
if (!$hash_list) {
return array();
}
return DiffusionQuery::loadHistoryForCommitIdentifiers(
$hash_list,
$drequest);
}
protected function getMercurialResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$commit_hash = $request->getValue('commit');
$path = $request->getValue('path');
$offset = $request->getValue('offset');
$limit = $request->getValue('limit');
$path = DiffusionPathIDQuery::normalizePath($path);
$path = ltrim($path, '/');
// NOTE: Older versions of Mercurial give different results for these
// commands (see T1268):
//
// $ hg log -- ''
// $ hg log
//
// All versions of Mercurial give different results for these commands
// (merge commits are excluded with the "." version):
//
// $ hg log -- .
// $ hg log
//
// If we don't have a path component in the query, omit it from the command
// entirely to avoid these inconsistencies.
// NOTE: When viewing the history of a file, we don't use "-b", because
// Mercurial stops history at the branchpoint but we're interested in all
// ancestors. When viewing history of a branch, we do use "-b", and thus
// stop history (this is more consistent with the Mercurial worldview of
// branches).
if (strlen($path)) {
$path_arg = csprintf('%s', $path);
$revset_arg = hgsprintf(
'reverse(ancestors(%s))',
$commit_hash);
} else {
$path_arg = '';
$revset_arg = hgsprintf(
'reverse(ancestors(%s)) and branch(%s)',
$drequest->getBranch(),
$commit_hash);
}
list($stdout) = $repository->execxLocalCommand(
'log --debug --template %s --limit %d --rev %s -- %C',
'{node};{parents}\\n',
($offset + $limit), // No '--skip' in Mercurial.
$revset_arg,
$path_arg);
$stdout = DiffusionMercurialCommandEngine::filterMercurialDebugOutput(
$stdout);
$lines = explode("\n", trim($stdout));
$lines = array_slice($lines, $offset);
$hash_list = array();
$parent_map = array();
$last = null;
foreach (array_reverse($lines) as $line) {
list($hash, $parents) = explode(';', $line);
$parents = trim($parents);
if (!$parents) {
if ($last === null) {
$parent_map[$hash] = array('...');
} else {
$parent_map[$hash] = array($last);
}
} else {
$parents = preg_split('/\s+/', $parents);
foreach ($parents as $parent) {
list($plocal, $phash) = explode(':', $parent);
if (!preg_match('/^0+$/', $phash)) {
$parent_map[$hash][] = $phash;
}
}
// This may happen for the zeroth commit in repository, both hashes
// are "000000000...".
if (empty($parent_map[$hash])) {
$parent_map[$hash] = array('...');
}
}
// The rendering code expects the first commit to be "mainline", like
// Git. Flip the order so it does the right thing.
$parent_map[$hash] = array_reverse($parent_map[$hash]);
$hash_list[] = $hash;
$last = $hash;
}
$hash_list = array_reverse($hash_list);
$this->parents = array_reverse($parent_map, true);
return DiffusionQuery::loadHistoryForCommitIdentifiers(
$hash_list,
$drequest);
}
protected function getSVNResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$commit = $request->getValue('commit');
$path = $request->getValue('path');
$offset = $request->getValue('offset');
$limit = $request->getValue('limit');
$need_direct_changes = $request->getValue('needDirectChanges');
$need_child_changes = $request->getValue('needChildChanges');
$conn_r = $repository->establishConnection('r');
$paths = queryfx_all(
$conn_r,
'SELECT id, path FROM %T WHERE pathHash IN (%Ls)',
PhabricatorRepository::TABLE_PATH,
array(md5('/'.trim($path, '/'))));
$paths = ipull($paths, 'id', 'path');
$path_id = idx($paths, '/'.trim($path, '/'));
if (!$path_id) {
return array();
}
- $filter_query = '';
+ $filter_query = qsprintf($conn_r, '');
if ($need_direct_changes) {
if ($need_child_changes) {
- $type = DifferentialChangeType::TYPE_CHILD;
- $filter_query = 'AND (isDirect = 1 OR changeType = '.$type.')';
+ $filter_query = qsprintf(
+ $conn_r,
+ 'AND (isDirect = 1 OR changeType = %s)',
+ DifferentialChangeType::TYPE_CHILD);
} else {
- $filter_query = 'AND (isDirect = 1)';
+ $filter_query = qsprintf(
+ $conn_r,
+ 'AND (isDirect = 1)');
}
}
$history_data = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE repositoryID = %d AND pathID = %d
AND commitSequence <= %d
%Q
ORDER BY commitSequence DESC
LIMIT %d, %d',
PhabricatorRepository::TABLE_PATHCHANGE,
$repository->getID(),
$path_id,
$commit ? $commit : 0x7FFFFFFF,
$filter_query,
$offset,
$limit);
$commits = array();
$commit_data = array();
$commit_ids = ipull($history_data, 'commitID');
if ($commit_ids) {
$commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(
'id IN (%Ld)',
$commit_ids);
if ($commits) {
$commit_data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(
'commitID in (%Ld)',
$commit_ids);
$commit_data = mpull($commit_data, null, 'getCommitID');
}
}
$history = array();
foreach ($history_data as $row) {
$item = new DiffusionPathChange();
$commit = idx($commits, $row['commitID']);
if ($commit) {
$item->setCommit($commit);
$item->setCommitIdentifier($commit->getCommitIdentifier());
$data = idx($commit_data, $commit->getID());
if ($data) {
$item->setCommitData($data);
}
}
$item->setChangeType($row['changeType']);
$item->setFileType($row['fileType']);
$history[] = $item;
}
return $history;
}
}
diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php
index 54be7dd7f..1493d658e 100644
--- a/src/applications/diffusion/controller/DiffusionBrowseController.php
+++ b/src/applications/diffusion/controller/DiffusionBrowseController.php
@@ -1,1125 +1,1129 @@
<?php
final class DiffusionBrowseController extends DiffusionController {
private $lintCommit;
private $lintMessages;
private $corpusButtons = array();
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$response = $this->loadDiffusionContext();
if ($response) {
return $response;
}
$drequest = $this->getDiffusionRequest();
// Figure out if we're browsing a directory, a file, or a search result
// list.
$grep = $request->getStr('grep');
if (strlen($grep)) {
return $this->browseSearch();
}
$pager = id(new PHUIPagerView())
->readFromRequest($request);
$results = DiffusionBrowseResultSet::newFromConduit(
$this->callConduitWithDiffusionRequest(
'diffusion.browsequery',
array(
'path' => $drequest->getPath(),
'commit' => $drequest->getStableCommit(),
'offset' => $pager->getOffset(),
'limit' => $pager->getPageSize() + 1,
)));
$reason = $results->getReasonForEmptyResultSet();
$is_file = ($reason == DiffusionBrowseResultSet::REASON_IS_FILE);
if ($is_file) {
return $this->browseFile();
}
$paths = $results->getPaths();
$paths = $pager->sliceResults($paths);
$results->setPaths($paths);
return $this->browseDirectory($results, $pager);
}
private function browseSearch() {
$drequest = $this->getDiffusionRequest();
$header = $this->buildHeaderView($drequest);
$path = nonempty(basename($drequest->getPath()), '/');
$search_results = $this->renderSearchResults();
$search_form = $this->renderSearchForm($path);
$search_form = phutil_tag(
'div',
array(
'class' => 'diffusion-mobile-search-form',
),
$search_form);
$crumbs = $this->buildCrumbs(
array(
'branch' => true,
'path' => true,
'view' => 'browse',
));
$crumbs->setBorder(true);
$tabs = $this->buildTabsView('code');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setTabs($tabs)
->setFooter(
array(
$search_form,
$search_results,
));
return $this->newPage()
->setTitle(
array(
nonempty(basename($drequest->getPath()), '/'),
$drequest->getRepository()->getDisplayName(),
))
->setCrumbs($crumbs)
->appendChild($view);
}
private function browseFile() {
$viewer = $this->getViewer();
$request = $this->getRequest();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$before = $request->getStr('before');
if ($before) {
return $this->buildBeforeResponse($before);
}
$path = $drequest->getPath();
$params = array(
'commit' => $drequest->getCommit(),
'path' => $drequest->getPath(),
);
$view = $request->getStr('view');
$byte_limit = null;
if ($view !== 'raw') {
$byte_limit = PhabricatorFileStorageEngine::getChunkThreshold();
$time_limit = 10;
$params += array(
'timeout' => $time_limit,
'byteLimit' => $byte_limit,
);
}
$response = $this->callConduitWithDiffusionRequest(
'diffusion.filecontentquery',
$params);
$hit_byte_limit = $response['tooHuge'];
$hit_time_limit = $response['tooSlow'];
$file_phid = $response['filePHID'];
$show_editor = false;
if ($hit_byte_limit) {
$corpus = $this->buildErrorCorpus(
pht(
'This file is larger than %s byte(s), and too large to display '.
'in the web UI.',
phutil_format_bytes($byte_limit)));
} else if ($hit_time_limit) {
$corpus = $this->buildErrorCorpus(
pht(
'This file took too long to load from the repository (more than '.
'%s second(s)).',
new PhutilNumber($time_limit)));
} else {
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($file_phid))
->executeOne();
if (!$file) {
throw new Exception(pht('Failed to load content file!'));
}
if ($view === 'raw') {
return $file->getRedirectResponse();
}
$data = $file->loadFileData();
$lfs_ref = $this->getGitLFSRef($repository, $data);
if ($lfs_ref) {
if ($view == 'git-lfs') {
$file = $this->loadGitLFSFile($lfs_ref);
// Rename the file locally so we generate a better vanity URI for
// it. In storage, it just has a name like "lfs-13f9a94c0923...",
// since we don't get any hints about possible human-readable names
// at upload time.
$basename = basename($drequest->getPath());
$file->makeEphemeral();
$file->setName($basename);
return $file->getRedirectResponse();
}
$corpus = $this->buildGitLFSCorpus($lfs_ref);
} else {
$show_editor = true;
$ref = id(new PhabricatorDocumentRef())
->setFile($file);
$engine = id(new DiffusionDocumentRenderingEngine())
->setRequest($request)
->setDiffusionRequest($drequest);
$corpus = $engine->newDocumentView($ref);
$this->corpusButtons[] = $this->renderFileButton();
}
}
$bar = $this->buildButtonBar($drequest, $show_editor);
$header = $this->buildHeaderView($drequest);
$header->setHeaderIcon('fa-file-code-o');
$follow = $request->getStr('follow');
$follow_notice = null;
if ($follow) {
$follow_notice = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setTitle(pht('Unable to Continue'));
switch ($follow) {
case 'first':
$follow_notice->appendChild(
pht(
'Unable to continue tracing the history of this file because '.
'this commit is the first commit in the repository.'));
break;
case 'created':
$follow_notice->appendChild(
pht(
'Unable to continue tracing the history of this file because '.
'this commit created the file.'));
break;
}
}
$renamed = $request->getStr('renamed');
$renamed_notice = null;
if ($renamed) {
$renamed_notice = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setTitle(pht('File Renamed'))
->appendChild(
pht(
'File history passes through a rename from "%s" to "%s".',
$drequest->getPath(),
$renamed));
}
$open_revisions = $this->buildOpenRevisions();
$owners_list = $this->buildOwnersList($drequest);
$crumbs = $this->buildCrumbs(
array(
'branch' => true,
'path' => true,
'view' => 'browse',
));
$crumbs->setBorder(true);
$basename = basename($this->getDiffusionRequest()->getPath());
$tabs = $this->buildTabsView('code');
$bar->setRight($this->corpusButtons);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setTabs($tabs)
->setFooter(array(
$bar,
$follow_notice,
$renamed_notice,
$corpus,
$open_revisions,
$owners_list,
));
$title = array($basename, $repository->getDisplayName());
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild(
array(
$view,
));
}
public function browseDirectory(
DiffusionBrowseResultSet $results,
PHUIPagerView $pager) {
$request = $this->getRequest();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$reason = $results->getReasonForEmptyResultSet();
$this->buildActionButtons($drequest, true);
$details = $this->buildPropertyView($drequest);
$header = $this->buildHeaderView($drequest);
$header->setHeaderIcon('fa-folder-open');
$empty_result = null;
$browse_panel = null;
$branch_panel = null;
if (!$results->isValidResults()) {
$empty_result = new DiffusionEmptyResultView();
$empty_result->setDiffusionRequest($drequest);
$empty_result->setDiffusionBrowseResultSet($results);
$empty_result->setView($request->getStr('view'));
} else {
$phids = array();
foreach ($results->getPaths() as $result) {
$data = $result->getLastCommitData();
if ($data) {
if ($data->getCommitDetail('authorPHID')) {
$phids[$data->getCommitDetail('authorPHID')] = true;
}
}
}
$phids = array_keys($phids);
$handles = $this->loadViewerHandles($phids);
$browse_table = id(new DiffusionBrowseTableView())
->setDiffusionRequest($drequest)
->setHandles($handles)
->setPaths($results->getPaths())
->setUser($request->getUser());
$title = nonempty(basename($drequest->getPath()), '/');
$icon = 'fa-folder-open';
$browse_header = $this->buildPanelHeaderView($title, $icon);
$browse_panel = id(new PHUIObjectBoxView())
->setHeader($browse_header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($browse_table)
->addClass('diffusion-mobile-view')
->setPager($pager);
$path = $drequest->getPath();
$is_branch = (!strlen($path) && $repository->supportsBranchComparison());
if ($is_branch) {
$branch_panel = $this->buildBranchTable();
}
}
$open_revisions = $this->buildOpenRevisions();
$readme = $this->renderDirectoryReadme($results);
$crumbs = $this->buildCrumbs(
array(
'branch' => true,
'path' => true,
'view' => 'browse',
));
$crumbs->setBorder(true);
$tabs = $this->buildTabsView('code');
$owners_list = $this->buildOwnersList($drequest);
$bar = id(new PHUILeftRightView())
->setRight($this->corpusButtons)
->addClass('diffusion-action-bar');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setTabs($tabs)
->setFooter(
array(
$bar,
$branch_panel,
$empty_result,
$browse_panel,
$open_revisions,
$owners_list,
$readme,
));
if ($details) {
$view->addPropertySection(pht('Details'), $details);
}
return $this->newPage()
->setTitle(array(
nonempty(basename($drequest->getPath()), '/'),
$repository->getDisplayName(),
))
->setCrumbs($crumbs)
->appendChild(
array(
$view,
));
}
private function renderSearchResults() {
$request = $this->getRequest();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$results = array();
$pager = id(new PHUIPagerView())
->readFromRequest($request);
$search_mode = null;
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$results = array();
break;
default:
if (strlen($this->getRequest()->getStr('grep'))) {
$search_mode = 'grep';
$query_string = $request->getStr('grep');
$results = $this->callConduitWithDiffusionRequest(
'diffusion.searchquery',
array(
'grep' => $query_string,
'commit' => $drequest->getStableCommit(),
'path' => $drequest->getPath(),
'limit' => $pager->getPageSize() + 1,
'offset' => $pager->getOffset(),
));
}
break;
}
$results = $pager->sliceResults($results);
$table = null;
$header = null;
if ($search_mode == 'grep') {
$table = $this->renderGrepResults($results, $query_string);
$title = pht(
'File content matching "%s" under "%s"',
$query_string,
nonempty($drequest->getPath(), '/'));
$header = id(new PHUIHeaderView())
->setHeader($title)
->addClass('diffusion-search-result-header');
}
return array($header, $table, $pager);
}
private function renderGrepResults(array $results, $pattern) {
$drequest = $this->getDiffusionRequest();
require_celerity_resource('phabricator-search-results-css');
if (!$results) {
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NODATA)
->appendChild(
pht(
'The pattern you searched for was not found in the content of any '.
'files.'));
}
$grouped = array();
foreach ($results as $file) {
list($path, $line, $string) = $file;
$grouped[$path][] = array($line, $string);
}
$view = array();
foreach ($grouped as $path => $matches) {
$view[] = id(new DiffusionPatternSearchView())
->setPath($path)
->setMatches($matches)
->setPattern($pattern)
->setDiffusionRequest($drequest)
->render();
}
return $view;
}
private function buildButtonBar(
DiffusionRequest $drequest,
$show_editor) {
$viewer = $this->getViewer();
$base_uri = $this->getRequest()->getRequestURI();
$user = $this->getRequest()->getUser();
$repository = $drequest->getRepository();
$path = $drequest->getPath();
$line = nonempty((int)$drequest->getLine(), 1);
$buttons = array();
$editor_link = $user->loadEditorLink($path, $line, $repository);
$template = $user->loadEditorLink($path, '%l', $repository);
$buttons[] =
id(new PHUIButtonView())
->setTag('a')
->setText(pht('Last Change'))
->setColor(PHUIButtonView::GREY)
->setHref(
$drequest->generateURI(
array(
'action' => 'change',
)))
->setIcon('fa-backward');
if ($editor_link) {
$buttons[] =
id(new PHUIButtonView())
->setTag('a')
->setText(pht('Open File'))
->setHref($editor_link)
->setIcon('fa-pencil')
->setID('editor_link')
->setMetadata(array('link_template' => $template))
->setDisabled(!$editor_link)
->setColor(PHUIButtonView::GREY);
}
$bar = id(new PHUILeftRightView())
->setLeft($buttons)
->addClass('diffusion-action-bar full-mobile-buttons');
return $bar;
}
private function buildOwnersList(DiffusionRequest $drequest) {
$viewer = $this->getViewer();
$have_owners = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorOwnersApplication',
$viewer);
if (!$have_owners) {
return null;
}
$repository = $drequest->getRepository();
$package_query = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
->withControl(
$repository->getPHID(),
array(
$drequest->getPath(),
));
$package_query->execute();
$packages = $package_query->getControllingPackagesForPath(
$repository->getPHID(),
$drequest->getPath());
$ownership = id(new PHUIObjectItemListView())
->setUser($viewer)
->setNoDataString(pht('No Owners'));
if ($packages) {
foreach ($packages as $package) {
$item = id(new PHUIObjectItemView())
->setObject($package)
->setObjectName($package->getMonogram())
->setHeader($package->getName())
->setHref($package->getURI());
$owners = $package->getOwners();
if ($owners) {
$owner_list = $viewer->renderHandleList(
mpull($owners, 'getUserPHID'));
} else {
$owner_list = phutil_tag('em', array(), pht('None'));
}
$item->addAttribute(pht('Owners: %s', $owner_list));
$auto = $package->getAutoReview();
$autoreview_map = PhabricatorOwnersPackage::getAutoreviewOptionsMap();
$spec = idx($autoreview_map, $auto, array());
$name = idx($spec, 'name', $auto);
$item->addIcon('fa-code', $name);
- if ($package->getAuditingEnabled()) {
- $item->addIcon('fa-check', pht('Auditing Enabled'));
- } else {
- $item->addIcon('fa-ban', pht('No Auditing'));
- }
+ $rule = $package->newAuditingRule();
+ $item->addIcon($rule->getIconIcon(), $rule->getDisplayName());
if ($package->isArchived()) {
$item->setDisabled(true);
}
$ownership->addItem($item);
}
}
$view = id(new PHUIObjectBoxView())
->setHeaderText(pht('Owner Packages'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addClass('diffusion-mobile-view')
->setObjectList($ownership);
return $view;
}
private function renderFileButton($file_uri = null, $label = null) {
$base_uri = $this->getRequest()->getRequestURI();
if ($file_uri) {
$text = pht('Download File');
$href = $file_uri;
$icon = 'fa-download';
} else {
$text = pht('Raw File');
$href = $base_uri->alter('view', 'raw');
$icon = 'fa-file-text';
}
if ($label !== null) {
$text = $label;
}
$button = id(new PHUIButtonView())
->setTag('a')
->setText($text)
->setHref($href)
->setIcon($icon)
->setColor(PHUIButtonView::GREY);
return $button;
}
private function renderGitLFSButton() {
$viewer = $this->getViewer();
$uri = $this->getRequest()->getRequestURI();
$href = $uri->alter('view', 'git-lfs');
$text = pht('Download from Git LFS');
$icon = 'fa-download';
return id(new PHUIButtonView())
->setTag('a')
->setText($text)
->setHref($href)
->setIcon($icon)
->setColor(PHUIButtonView::GREY);
}
private function buildErrorCorpus($message) {
$text = id(new PHUIBoxView())
->addPadding(PHUI::PADDING_LARGE)
->appendChild($message);
$header = id(new PHUIHeaderView())
->setHeader(pht('Details'));
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($text);
return $box;
}
private function buildBeforeResponse($before) {
$request = $this->getRequest();
$drequest = $this->getDiffusionRequest();
// NOTE: We need to get the grandparent so we can capture filename changes
// in the parent.
$parent = $this->loadParentCommitOf($before);
$old_filename = null;
$was_created = false;
if ($parent) {
$grandparent = $this->loadParentCommitOf($parent);
if ($grandparent) {
$rename_query = new DiffusionRenameHistoryQuery();
$rename_query->setRequest($drequest);
$rename_query->setOldCommit($grandparent);
$rename_query->setViewer($request->getUser());
$old_filename = $rename_query->loadOldFilename();
$was_created = $rename_query->getWasCreated();
}
}
$follow = null;
if ($was_created) {
// If the file was created in history, that means older commits won't
// have it. Since we know it existed at 'before', it must have been
// created then; jump there.
$target_commit = $before;
$follow = 'created';
} else if ($parent) {
// If we found a parent, jump to it. This is the normal case.
$target_commit = $parent;
} else {
// If there's no parent, this was probably created in the initial commit?
// And the "was_created" check will fail because we can't identify the
// grandparent. Keep the user at 'before'.
$target_commit = $before;
$follow = 'first';
}
$path = $drequest->getPath();
$renamed = null;
if ($old_filename !== null &&
$old_filename !== '/'.$path) {
$renamed = $path;
$path = $old_filename;
}
$line = null;
// If there's a follow error, drop the line so the user sees the message.
if (!$follow) {
$line = $this->getBeforeLineNumber($target_commit);
}
$before_uri = $drequest->generateURI(
array(
'action' => 'browse',
'commit' => $target_commit,
'line' => $line,
'path' => $path,
));
- $before_uri->setQueryParams($request->getRequestURI()->getQueryParams());
- $before_uri = $before_uri->alter('before', null);
- $before_uri = $before_uri->alter('renamed', $renamed);
- $before_uri = $before_uri->alter('follow', $follow);
+ if ($renamed === null) {
+ $before_uri->removeQueryParam('renamed');
+ } else {
+ $before_uri->replaceQueryParam('renamed', $renamed);
+ }
+
+ if ($follow === null) {
+ $before_uri->removeQueryParam('follow');
+ } else {
+ $before_uri->replaceQueryParam('follow', $follow);
+ }
return id(new AphrontRedirectResponse())->setURI($before_uri);
}
private function getBeforeLineNumber($target_commit) {
$drequest = $this->getDiffusionRequest();
$viewer = $this->getViewer();
$line = $drequest->getLine();
if (!$line) {
return null;
}
$diff_info = $this->callConduitWithDiffusionRequest(
'diffusion.rawdiffquery',
array(
'commit' => $drequest->getCommit(),
'path' => $drequest->getPath(),
'againstCommit' => $target_commit,
));
$file_phid = $diff_info['filePHID'];
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($file_phid))
->executeOne();
if (!$file) {
throw new Exception(
pht(
'Failed to load file ("%s") returned by "%s".',
$file_phid,
'diffusion.rawdiffquery.'));
}
$raw_diff = $file->loadFileData();
$old_line = 0;
$new_line = 0;
foreach (explode("\n", $raw_diff) as $text) {
if ($text[0] == '-' || $text[0] == ' ') {
$old_line++;
}
if ($text[0] == '+' || $text[0] == ' ') {
$new_line++;
}
if ($new_line == $line) {
return $old_line;
}
}
// We didn't find the target line.
return $line;
}
private function loadParentCommitOf($commit) {
$drequest = $this->getDiffusionRequest();
$user = $this->getRequest()->getUser();
$before_req = DiffusionRequest::newFromDictionary(
array(
'user' => $user,
'repository' => $drequest->getRepository(),
'commit' => $commit,
));
$parents = DiffusionQuery::callConduitWithDiffusionRequest(
$user,
$before_req,
'diffusion.commitparentsquery',
array(
'commit' => $commit,
));
return head($parents);
}
protected function markupText($text) {
$engine = PhabricatorMarkupEngine::newDiffusionMarkupEngine();
$engine->setConfig('viewer', $this->getRequest()->getUser());
$text = $engine->markupText($text);
$text = phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$text);
return $text;
}
protected function buildHeaderView(DiffusionRequest $drequest) {
$viewer = $this->getViewer();
$repository = $drequest->getRepository();
$commit_tag = $this->renderCommitHashTag($drequest);
$path = nonempty($drequest->getPath(), '/');
$search = $this->renderSearchForm($path);
$header = id(new PHUIHeaderView())
->setUser($viewer)
->setHeader($this->renderPathLinks($drequest, $mode = 'browse'))
->addActionItem($search)
->addTag($commit_tag)
->addClass('diffusion-browse-header');
if (!$repository->isSVN()) {
$branch_tag = $this->renderBranchTag($drequest);
$header->addTag($branch_tag);
}
return $header;
}
protected function buildPanelHeaderView($title, $icon) {
$header = id(new PHUIHeaderView())
->setHeader($title)
->setHeaderIcon($icon)
->addClass('diffusion-panel-header-view');
return $header;
}
protected function buildActionButtons(
DiffusionRequest $drequest,
$is_directory = false) {
$viewer = $this->getViewer();
$repository = $drequest->getRepository();
$history_uri = $drequest->generateURI(array('action' => 'history'));
$behind_head = $drequest->getSymbolicCommit();
$compare = null;
$head_uri = $drequest->generateURI(
array(
'commit' => '',
'action' => 'browse',
));
if ($repository->supportsBranchComparison() && $is_directory) {
$compare_uri = $drequest->generateURI(array('action' => 'compare'));
$compare = id(new PHUIButtonView())
->setText(pht('Compare'))
->setIcon('fa-code-fork')
->setWorkflow(true)
->setTag('a')
->setHref($compare_uri)
->setColor(PHUIButtonView::GREY);
$this->corpusButtons[] = $compare;
}
$head = null;
if ($behind_head) {
$head = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Back to HEAD'))
->setHref($head_uri)
->setIcon('fa-home')
->setColor(PHUIButtonView::GREY);
$this->corpusButtons[] = $head;
}
$history = id(new PHUIButtonView())
->setText(pht('History'))
->setHref($history_uri)
->setTag('a')
->setIcon('fa-history')
->setColor(PHUIButtonView::GREY);
$this->corpusButtons[] = $history;
}
protected function buildPropertyView(
DiffusionRequest $drequest) {
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setUser($viewer);
if ($drequest->getSymbolicType() == 'tag') {
$symbolic = $drequest->getSymbolicCommit();
$view->addProperty(pht('Tag'), $symbolic);
$tags = $this->callConduitWithDiffusionRequest(
'diffusion.tagsquery',
array(
'names' => array($symbolic),
'needMessages' => true,
));
$tags = DiffusionRepositoryTag::newFromConduit($tags);
$tags = mpull($tags, null, 'getName');
$tag = idx($tags, $symbolic);
if ($tag && strlen($tag->getMessage())) {
$view->addSectionHeader(
pht('Tag Content'), 'fa-tag');
$view->addTextContent($this->markupText($tag->getMessage()));
}
}
if ($view->hasAnyProperties()) {
return $view;
}
return null;
}
private function buildOpenRevisions() {
$viewer = $this->getViewer();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$path = $drequest->getPath();
$path_map = id(new DiffusionPathIDQuery(array($path)))->loadPathIDs();
$path_id = idx($path_map, $path);
if (!$path_id) {
return null;
}
$recent = (PhabricatorTime::getNow() - phutil_units('30 days in seconds'));
$revisions = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withPath($repository->getID(), $path_id)
->withIsOpen(true)
->withUpdatedEpochBetween($recent, null)
->setOrder(DifferentialRevisionQuery::ORDER_MODIFIED)
->setLimit(10)
->needReviewers(true)
->needFlags(true)
->needDrafts(true)
->execute();
if (!$revisions) {
return null;
}
$header = id(new PHUIHeaderView())
->setHeader(pht('Recently Open Revisions'));
$list = id(new DifferentialRevisionListView())
->setViewer($viewer)
->setRevisions($revisions)
->setNoBox(true);
$view = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addClass('diffusion-mobile-view')
->appendChild($list);
return $view;
}
private function getGitLFSRef(PhabricatorRepository $repository, $data) {
if (!$repository->canUseGitLFS()) {
return null;
}
$lfs_pattern = '(^version https://git-lfs\\.github\\.com/spec/v1[\r\n])';
if (!preg_match($lfs_pattern, $data)) {
return null;
}
$matches = null;
if (!preg_match('(^oid sha256:(.*)$)m', $data, $matches)) {
return null;
}
$hash = $matches[1];
$hash = trim($hash);
return id(new PhabricatorRepositoryGitLFSRefQuery())
->setViewer($this->getViewer())
->withRepositoryPHIDs(array($repository->getPHID()))
->withObjectHashes(array($hash))
->executeOne();
}
private function buildGitLFSCorpus(PhabricatorRepositoryGitLFSRef $ref) {
// TODO: We should probably test if we can load the file PHID here and
// show the user an error if we can't, rather than making them click
// through to hit an error.
$title = basename($this->getDiffusionRequest()->getPath());
$icon = 'fa-archive';
$drequest = $this->getDiffusionRequest();
$this->buildActionButtons($drequest);
$header = $this->buildPanelHeaderView($title, $icon);
$severity = PHUIInfoView::SEVERITY_NOTICE;
$messages = array();
$messages[] = pht(
'This %s file is stored in Git Large File Storage.',
phutil_format_bytes($ref->getByteSize()));
try {
$file = $this->loadGitLFSFile($ref);
$this->corpusButtons[] = $this->renderGitLFSButton();
} catch (Exception $ex) {
$severity = PHUIInfoView::SEVERITY_ERROR;
$messages[] = pht('The data for this file could not be loaded.');
}
$this->corpusButtons[] = $this->renderFileButton(
null, pht('View Raw LFS Pointer'));
$corpus = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addClass('diffusion-mobile-view')
->setCollapsed(true);
if ($messages) {
$corpus->setInfoView(
id(new PHUIInfoView())
->setSeverity($severity)
->setErrors($messages));
}
return $corpus;
}
private function loadGitLFSFile(PhabricatorRepositoryGitLFSRef $ref) {
$viewer = $this->getViewer();
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($ref->getFilePHID()))
->executeOne();
if (!$file) {
throw new Exception(
pht(
'Failed to load file object for Git LFS ref "%s"!',
$ref->getObjectHash()));
}
return $file;
}
private function buildBranchTable() {
$viewer = $this->getViewer();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$branch = $drequest->getBranch();
$default_branch = $repository->getDefaultBranch();
if ($branch === $default_branch) {
return null;
}
$pager = id(new PHUIPagerView())
->setPageSize(10);
try {
$results = $this->callConduitWithDiffusionRequest(
'diffusion.historyquery',
array(
'commit' => $branch,
'against' => $default_branch,
'path' => $drequest->getPath(),
'offset' => $pager->getOffset(),
'limit' => $pager->getPageSize() + 1,
));
} catch (Exception $ex) {
return null;
}
$history = DiffusionPathChange::newFromConduit($results['pathChanges']);
$history = $pager->sliceResults($history);
if (!$history) {
return null;
}
$history_table = id(new DiffusionHistoryTableView())
->setViewer($viewer)
->setDiffusionRequest($drequest)
->setHistory($history);
$history_table->loadRevisions();
$history_table
->setParents($results['parents'])
->setFilterParents(true)
->setIsHead(true)
->setIsTail(!$pager->getHasMorePages());
$header = id(new PHUIHeaderView())
->setHeader(pht('%s vs %s', $branch, $default_branch));
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addClass('diffusion-mobile-view')
->setTable($history_table);
}
}
diff --git a/src/applications/diffusion/controller/DiffusionChangeController.php b/src/applications/diffusion/controller/DiffusionChangeController.php
index 90258134c..0120c978d 100644
--- a/src/applications/diffusion/controller/DiffusionChangeController.php
+++ b/src/applications/diffusion/controller/DiffusionChangeController.php
@@ -1,186 +1,183 @@
<?php
final class DiffusionChangeController extends DiffusionController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$response = $this->loadDiffusionContext();
if ($response) {
return $response;
}
$viewer = $this->getViewer();
$drequest = $this->getDiffusionRequest();
$data = $this->callConduitWithDiffusionRequest(
'diffusion.diffquery',
array(
'commit' => $drequest->getCommit(),
'path' => $drequest->getPath(),
));
$drequest->updateSymbolicCommit($data['effectiveCommit']);
$raw_changes = ArcanistDiffChange::newFromConduit($data['changes']);
$diff = DifferentialDiff::newEphemeralFromRawChanges(
$raw_changes);
$changesets = $diff->getChangesets();
$changeset = reset($changesets);
if (!$changeset) {
// TODO: Refine this.
return new Aphront404Response();
}
$repository = $drequest->getRepository();
$changesets = array(
0 => $changeset,
);
$changeset_header = $this->buildChangesetHeader($drequest);
$changeset_view = new DifferentialChangesetListView();
$changeset_view->setChangesets($changesets);
$changeset_view->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
$changeset_view->setVisibleChangesets($changesets);
$changeset_view->setRenderingReferences(
array(
0 => $drequest->generateURI(array('action' => 'rendering-ref')),
));
$raw_params = array(
'action' => 'browse',
'params' => array(
'view' => 'raw',
),
);
$right_uri = $drequest->generateURI($raw_params);
$raw_params['params']['before'] = $drequest->getStableCommit();
$left_uri = $drequest->generateURI($raw_params);
$changeset_view->setRawFileURIs($left_uri, $right_uri);
$changeset_view->setRenderURI($repository->getPathURI('diff/'));
-
- $changeset_view->setWhitespace(
- DifferentialChangesetParser::WHITESPACE_SHOW_ALL);
$changeset_view->setUser($viewer);
$changeset_view->setHeader($changeset_header);
// TODO: This is pretty awkward, unify the CSS between Diffusion and
// Differential better.
require_celerity_resource('differential-core-view-css');
$crumbs = $this->buildCrumbs(
array(
'branch' => true,
'path' => true,
'view' => 'change',
));
$crumbs->setBorder(true);
$links = $this->renderPathLinks($drequest, $mode = 'browse');
$header = $this->buildHeader($drequest, $links);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setMainColumn(array(
))
->setFooter(array(
$changeset_view,
));
return $this->newPage()
->setTitle(
array(
basename($drequest->getPath()),
$repository->getDisplayName(),
))
->setCrumbs($crumbs)
->appendChild(
array(
$view,
));
}
private function buildHeader(
DiffusionRequest $drequest,
$links) {
$viewer = $this->getViewer();
$tag = $this->renderCommitHashTag($drequest);
$header = id(new PHUIHeaderView())
->setHeader($links)
->setUser($viewer)
->setPolicyObject($drequest->getRepository())
->addTag($tag);
return $header;
}
private function buildChangesetHeader(DiffusionRequest $drequest) {
$viewer = $this->getViewer();
$header = id(new PHUIHeaderView())
->setHeader(pht('Changes'));
$history_uri = $drequest->generateURI(
array(
'action' => 'history',
));
$header->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setText(pht('View History'))
->setHref($history_uri)
->setIcon('fa-clock-o'));
$browse_uri = $drequest->generateURI(
array(
'action' => 'browse',
));
$header->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setText(pht('Browse Content'))
->setHref($browse_uri)
->setIcon('fa-files-o'));
return $header;
}
protected function buildPropertyView(
DiffusionRequest $drequest,
PhabricatorActionListView $actions) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setActionList($actions);
$stable_commit = $drequest->getStableCommit();
$view->addProperty(
pht('Commit'),
phutil_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => 'commit',
'commit' => $stable_commit,
)),
),
$drequest->getRepository()->formatCommitName($stable_commit)));
return $view;
}
}
diff --git a/src/applications/diffusion/controller/DiffusionController.php b/src/applications/diffusion/controller/DiffusionController.php
index 54a575611..da5892396 100644
--- a/src/applications/diffusion/controller/DiffusionController.php
+++ b/src/applications/diffusion/controller/DiffusionController.php
@@ -1,592 +1,591 @@
<?php
abstract class DiffusionController extends PhabricatorController {
private $diffusionRequest;
protected function getDiffusionRequest() {
if (!$this->diffusionRequest) {
throw new PhutilInvalidStateException('loadDiffusionContext');
}
return $this->diffusionRequest;
}
protected function hasDiffusionRequest() {
return (bool)$this->diffusionRequest;
}
public function willBeginExecution() {
$request = $this->getRequest();
// Check if this is a VCS request, e.g. from "git clone", "hg clone", or
// "svn checkout". If it is, we jump off into repository serving code to
// process the request.
$serve_controller = new DiffusionServeController();
if ($serve_controller->isVCSRequest($request)) {
return $this->delegateToController($serve_controller);
}
return parent::willBeginExecution();
}
protected function loadDiffusionContextForEdit() {
return $this->loadContext(
array(
'edit' => true,
));
}
protected function loadDiffusionContext() {
return $this->loadContext(array());
}
private function loadContext(array $options) {
$request = $this->getRequest();
$viewer = $this->getViewer();
require_celerity_resource('diffusion-repository-css');
$identifier = $this->getRepositoryIdentifierFromRequest($request);
$params = $options + array(
'repository' => $identifier,
'user' => $viewer,
'blob' => $this->getDiffusionBlobFromRequest($request),
'commit' => $request->getURIData('commit'),
'path' => $request->getURIData('path'),
'line' => $request->getURIData('line'),
'branch' => $request->getURIData('branch'),
'lint' => $request->getStr('lint'),
);
$drequest = DiffusionRequest::newFromDictionary($params);
if (!$drequest) {
return new Aphront404Response();
}
// If the client is making a request like "/diffusion/1/...", but the
// repository has a different canonical path like "/diffusion/XYZ/...",
// redirect them to the canonical path.
// Skip this redirect if the request is an AJAX request, like the requests
// that Owners makes to complete and validate paths.
if (!$request->isAjax()) {
$request_path = $request->getPath();
$repository = $drequest->getRepository();
$canonical_path = $repository->getCanonicalPath($request_path);
if ($canonical_path !== null) {
if ($canonical_path != $request_path) {
return id(new AphrontRedirectResponse())->setURI($canonical_path);
}
}
}
$this->diffusionRequest = $drequest;
return null;
}
protected function getDiffusionBlobFromRequest(AphrontRequest $request) {
return $request->getURIData('dblob');
}
protected function getRepositoryIdentifierFromRequest(
AphrontRequest $request) {
$short_name = $request->getURIData('repositoryShortName');
if (strlen($short_name)) {
// If the short name ends in ".git", ignore it.
$short_name = preg_replace('/\\.git\z/', '', $short_name);
return $short_name;
}
$identifier = $request->getURIData('repositoryCallsign');
if (strlen($identifier)) {
return $identifier;
}
$id = $request->getURIData('repositoryID');
if (strlen($id)) {
return (int)$id;
}
return null;
}
public function buildCrumbs(array $spec = array()) {
$crumbs = $this->buildApplicationCrumbs();
$crumb_list = $this->buildCrumbList($spec);
foreach ($crumb_list as $crumb) {
$crumbs->addCrumb($crumb);
}
return $crumbs;
}
private function buildCrumbList(array $spec = array()) {
$spec = $spec + array(
'commit' => null,
'tags' => null,
'branches' => null,
'view' => null,
);
$crumb_list = array();
// On the home page, we don't have a DiffusionRequest.
if ($this->hasDiffusionRequest()) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
} else {
$drequest = null;
$repository = null;
}
if (!$repository) {
return $crumb_list;
}
$repository_name = $repository->getName();
if (!$spec['commit'] && !$spec['tags'] && !$spec['branches']) {
$branch_name = $drequest->getBranch();
if (strlen($branch_name)) {
$repository_name .= ' ('.$branch_name.')';
}
}
$crumb = id(new PHUICrumbView())
->setName($repository_name);
if (!$spec['view'] && !$spec['commit'] &&
!$spec['tags'] && !$spec['branches']) {
$crumb_list[] = $crumb;
return $crumb_list;
}
$crumb->setHref(
$drequest->generateURI(
array(
'action' => 'branch',
'path' => '/',
)));
$crumb_list[] = $crumb;
$stable_commit = $drequest->getStableCommit();
$commit_name = $repository->formatCommitName($stable_commit, $local = true);
$commit_uri = $repository->getCommitURI($stable_commit);
if ($spec['tags']) {
$crumb = new PHUICrumbView();
if ($spec['commit']) {
$crumb->setName(pht('Tags for %s', $commit_name));
$crumb->setHref($commit_uri);
} else {
$crumb->setName(pht('Tags'));
}
$crumb_list[] = $crumb;
return $crumb_list;
}
if ($spec['branches']) {
$crumb = id(new PHUICrumbView())
->setName(pht('Branches'));
$crumb_list[] = $crumb;
return $crumb_list;
}
if ($spec['commit']) {
$crumb = id(new PHUICrumbView())
->setName($commit_name);
$crumb_list[] = $crumb;
return $crumb_list;
}
$crumb = new PHUICrumbView();
$view = $spec['view'];
switch ($view) {
case 'history':
$view_name = pht('History');
break;
case 'graph':
$view_name = pht('Graph');
break;
case 'jobs': // c4science custo
$view_name = pht('Jenkins Job');
break;
case 'browse':
$view_name = pht('Browse');
break;
case 'lint':
$view_name = pht('Lint');
break;
case 'change':
$view_name = pht('Change');
break;
case 'compare':
$view_name = pht('Compare');
break;
}
$crumb = id(new PHUICrumbView())
->setName($view_name);
$crumb_list[] = $crumb;
return $crumb_list;
}
protected function callConduitWithDiffusionRequest(
$method,
array $params = array()) {
$user = $this->getRequest()->getUser();
$drequest = $this->getDiffusionRequest();
return DiffusionQuery::callConduitWithDiffusionRequest(
$user,
$drequest,
$method,
$params);
}
protected function callConduitMethod($method, array $params = array()) {
$user = $this->getViewer();
$drequest = $this->getDiffusionRequest();
return DiffusionQuery::callConduitWithDiffusionRequest(
$user,
$drequest,
$method,
$params,
true);
}
protected function getRepositoryControllerURI(
PhabricatorRepository $repository,
$path) {
return $repository->getPathURI($path);
}
protected function renderPathLinks(DiffusionRequest $drequest, $action) {
$path = $drequest->getPath();
$path_parts = array_filter(explode('/', trim($path, '/')));
$divider = phutil_tag(
'span',
array(
'class' => 'phui-header-divider',
),
'/');
$links = array();
if ($path_parts) {
$links[] = phutil_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => $action,
'path' => '',
)),
),
$drequest->getRepository()->getDisplayName());
$links[] = $divider;
$accum = '';
$last_key = last_key($path_parts);
foreach ($path_parts as $key => $part) {
$accum .= '/'.$part;
if ($key === $last_key) {
$links[] = $part;
} else {
$links[] = phutil_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => $action,
'path' => $accum.'/',
)),
),
$part);
$links[] = $divider;
}
}
} else {
$links[] = $drequest->getRepository()->getDisplayName();
$links[] = $divider;
}
return $links;
}
protected function renderStatusMessage($title, $body) {
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setTitle($title)
->setFlush(true)
->appendChild($body);
}
protected function renderCommitHashTag(DiffusionRequest $drequest) {
$stable_commit = $drequest->getStableCommit();
$commit = phutil_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => 'commit',
'commit' => $stable_commit,
)),
),
$drequest->getRepository()->formatCommitName($stable_commit, true));
$tag = id(new PHUITagView())
->setName($commit)
->setColor(PHUITagView::COLOR_INDIGO)
->setBorder(PHUITagView::BORDER_NONE)
->setType(PHUITagView::TYPE_SHADE);
return $tag;
}
protected function renderBranchTag(DiffusionRequest $drequest) {
$branch = $drequest->getBranch();
$branch = id(new PhutilUTF8StringTruncator())
->setMaximumGlyphs(24)
->truncateString($branch);
$tag = id(new PHUITagView())
->setName($branch)
->setColor(PHUITagView::COLOR_INDIGO)
->setBorder(PHUITagView::BORDER_NONE)
->setType(PHUITagView::TYPE_OUTLINE)
->addClass('diffusion-header-branch-tag');
return $tag;
}
protected function renderSymbolicCommit(DiffusionRequest $drequest) {
$symbolic_tag = $drequest->getSymbolicCommit();
$symbolic_tag = id(new PhutilUTF8StringTruncator())
->setMaximumGlyphs(24)
->truncateString($symbolic_tag);
$tag = id(new PHUITagView())
->setName($symbolic_tag)
->setIcon('fa-tag')
->setColor(PHUITagView::COLOR_INDIGO)
->setBorder(PHUITagView::BORDER_NONE)
->setType(PHUITagView::TYPE_SHADE);
return $tag;
}
protected function renderDirectoryReadme(DiffusionBrowseResultSet $browse) {
$readme_path = $browse->getReadmePath();
if ($readme_path === null) {
return null;
}
$drequest = $this->getDiffusionRequest();
$viewer = $this->getViewer();
$repository = $drequest->getRepository();
$repository_phid = $repository->getPHID();
$stable_commit = $drequest->getStableCommit();
$stable_commit_hash = PhabricatorHash::digestForIndex($stable_commit);
$readme_path_hash = PhabricatorHash::digestForIndex($readme_path);
$cache = PhabricatorCaches::getMutableStructureCache();
$cache_key = "diffusion".
".repository({$repository_phid})".
".commit({$stable_commit_hash})".
".readme({$readme_path_hash})";
$readme_cache = $cache->getKey($cache_key);
if (!$readme_cache) {
try {
$result = $this->callConduitWithDiffusionRequest(
'diffusion.filecontentquery',
array(
'path' => $readme_path,
'commit' => $drequest->getStableCommit(),
));
} catch (Exception $ex) {
return null;
}
$file_phid = $result['filePHID'];
if (!$file_phid) {
return null;
}
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($file_phid))
->executeOne();
if (!$file) {
return null;
}
$corpus = $file->loadFileData();
$readme_cache = array(
'corpus' => $corpus,
);
$cache->setKey($cache_key, $readme_cache);
}
$readme_corpus = $readme_cache['corpus'];
if (!strlen($readme_corpus)) {
return null;
}
return id(new DiffusionReadmeView())
->setUser($this->getViewer())
->setPath($readme_path)
->setContent($readme_corpus);
}
protected function renderSearchForm($path = '/') {
$drequest = $this->getDiffusionRequest();
$viewer = $this->getViewer();
switch ($drequest->getRepository()->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
return null;
}
$search_term = $this->getRequest()->getStr('grep');
require_celerity_resource('diffusion-icons-css');
require_celerity_resource('diffusion-css');
$href = $drequest->generateURI(array(
'action' => 'browse',
'path' => $path,
));
$bar = javelin_tag(
'input',
array(
'type' => 'text',
'id' => 'diffusion-search-input',
'name' => 'grep',
'class' => 'diffusion-search-input',
'sigil' => 'diffusion-search-input',
'placeholder' => pht('Pattern Search'),
'value' => $search_term,
));
$form = phabricator_form(
$viewer,
array(
'method' => 'GET',
'action' => $href,
'sigil' => 'diffusion-search-form',
'class' => 'diffusion-search-form',
'id' => 'diffusion-search-form',
),
array(
$bar,
));
$form_view = phutil_tag(
'div',
array(
'class' => 'diffusion-search-form-view',
),
$form);
return $form_view;
}
protected function buildTabsView($key) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$view = new PHUIListView();
$view->addMenuItem(
id(new PHUIListItemView())
->setKey('code')
->setName(pht('Code'))
->setIcon('fa-code')
->setHref($drequest->generateURI(
array(
- 'action' => 'branch',
- 'path' => '/',
+ 'action' => 'browse',
)))
->setSelected($key == 'code'));
if (!$repository->isSVN()) {
$view->addMenuItem(
id(new PHUIListItemView())
->setKey('branch')
->setName(pht('Branches'))
->setIcon('fa-code-fork')
->setHref($drequest->generateURI(
array(
'action' => 'branches',
)))
->setSelected($key == 'branch'));
}
if (!$repository->isSVN()) {
$view->addMenuItem(
id(new PHUIListItemView())
->setKey('tags')
->setName(pht('Tags'))
->setIcon('fa-tags')
->setHref($drequest->generateURI(
array(
'action' => 'tags',
)))
->setSelected($key == 'tags'));
}
$view->addMenuItem(
id(new PHUIListItemView())
->setKey('history')
->setName(pht('History'))
->setIcon('fa-history')
->setHref($drequest->generateURI(
array(
'action' => 'history',
)))
->setSelected($key == 'history'));
$view->addMenuItem(
id(new PHUIListItemView())
->setKey('graph')
->setName(pht('Graph'))
->setIcon('fa-code-fork')
->setHref($drequest->generateURI(
array(
'action' => 'graph',
)))
->setSelected($key == 'graph'));
// c4science customization
$viewer = $this->getViewer();
$have_jenkins = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorJenkinsJobApplication', $viewer);
if ($have_jenkins) {
$view->addMenuItem(
id(new PHUIListItemView())
->setKey('jobs')
->setName(pht('Jenkins Jobs'))
->setIcon('fa-cogs')
->setHref($drequest->generateURI(
array(
'action' => 'jobs',
)))
->setSelected($key == 'jobs'));
}
return $view;
}
}
diff --git a/src/applications/diffusion/controller/DiffusionDiffController.php b/src/applications/diffusion/controller/DiffusionDiffController.php
index 86409c6fa..a0111d001 100644
--- a/src/applications/diffusion/controller/DiffusionDiffController.php
+++ b/src/applications/diffusion/controller/DiffusionDiffController.php
@@ -1,133 +1,130 @@
<?php
final class DiffusionDiffController extends DiffusionController {
public function shouldAllowPublic() {
return true;
}
protected function getDiffusionBlobFromRequest(AphrontRequest $request) {
return $request->getStr('ref');
}
public function handleRequest(AphrontRequest $request) {
$response = $this->loadDiffusionContext();
if ($response) {
return $response;
}
$viewer = $this->getViewer();
$drequest = $this->getDiffusionRequest();
if (!$request->isAjax()) {
// This request came out of the dropdown menu, either "View Standalone"
// or "View Raw File".
$view = $request->getStr('view');
if ($view == 'r') {
$uri = $drequest->generateURI(
array(
'action' => 'browse',
'params' => array(
'view' => 'raw',
),
));
} else {
$uri = $drequest->generateURI(
array(
'action' => 'change',
));
}
return id(new AphrontRedirectResponse())->setURI($uri);
}
$data = $this->callConduitWithDiffusionRequest(
'diffusion.diffquery',
array(
'commit' => $drequest->getCommit(),
'path' => $drequest->getPath(),
));
$drequest->updateSymbolicCommit($data['effectiveCommit']);
$raw_changes = ArcanistDiffChange::newFromConduit($data['changes']);
$diff = DifferentialDiff::newEphemeralFromRawChanges(
$raw_changes);
$changesets = $diff->getChangesets();
$changeset = reset($changesets);
if (!$changeset) {
return new Aphront404Response();
}
$parser = new DifferentialChangesetParser();
$parser->setUser($viewer);
$parser->setChangeset($changeset);
$parser->setRenderingReference($drequest->generateURI(
array(
'action' => 'rendering-ref',
)));
$parser->readParametersFromRequest($request);
$coverage = $drequest->loadCoverage();
if ($coverage) {
$parser->setCoverage($coverage);
}
$commit = $drequest->loadCommit();
$pquery = new DiffusionPathIDQuery(array($changeset->getFilename()));
$ids = $pquery->loadPathIDs();
$path_id = $ids[$changeset->getFilename()];
$parser->setLeftSideCommentMapping($path_id, false);
$parser->setRightSideCommentMapping($path_id, true);
$parser->setCanMarkDone(
($commit->getAuthorPHID()) &&
($viewer->getPHID() == $commit->getAuthorPHID()));
$parser->setObjectOwnerPHID($commit->getAuthorPHID());
- $parser->setWhitespaceMode(
- DifferentialChangesetParser::WHITESPACE_SHOW_ALL);
-
$inlines = PhabricatorAuditInlineComment::loadDraftAndPublishedComments(
$viewer,
$commit->getPHID(),
$path_id);
if ($inlines) {
foreach ($inlines as $inline) {
$parser->parseInlineComment($inline);
}
$phids = mpull($inlines, 'getAuthorPHID');
$handles = $this->loadViewerHandles($phids);
$parser->setHandles($handles);
}
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($viewer);
foreach ($inlines as $inline) {
$engine->addObject(
$inline,
PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY);
}
$engine->process();
$parser->setMarkupEngine($engine);
$spec = $request->getStr('range');
list($range_s, $range_e, $mask) =
DifferentialChangesetParser::parseRangeSpecification($spec);
$parser->setRange($range_s, $range_e);
$parser->setMask($mask);
return id(new PhabricatorChangesetResponse())
->setRenderedChangeset($parser->renderChangeset())
->setUndoTemplates($parser->getRenderer()->renderUndoTemplates());
}
}
diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php
index cb4ad0ba9..aea901f10 100644
--- a/src/applications/diffusion/controller/DiffusionServeController.php
+++ b/src/applications/diffusion/controller/DiffusionServeController.php
@@ -1,1242 +1,1266 @@
<?php
final class DiffusionServeController extends DiffusionController {
private $serviceViewer;
private $serviceRepository;
private $isGitLFSRequest;
private $gitLFSToken;
private $gitLFSInput;
public function setServiceViewer(PhabricatorUser $viewer) {
$this->getRequest()->setUser($viewer);
$this->serviceViewer = $viewer;
return $this;
}
public function getServiceViewer() {
return $this->serviceViewer;
}
public function setServiceRepository(PhabricatorRepository $repository) {
$this->serviceRepository = $repository;
return $this;
}
public function getServiceRepository() {
return $this->serviceRepository;
}
public function getIsGitLFSRequest() {
return $this->isGitLFSRequest;
}
public function getGitLFSToken() {
return $this->gitLFSToken;
}
public function isVCSRequest(AphrontRequest $request) {
$identifier = $this->getRepositoryIdentifierFromRequest($request);
if ($identifier === null) {
return null;
}
$content_type = $request->getHTTPHeader('Content-Type');
$user_agent = idx($_SERVER, 'HTTP_USER_AGENT');
$request_type = $request->getHTTPHeader('X-Phabricator-Request-Type');
// This may have a "charset" suffix, so only match the prefix.
$lfs_pattern = '(^application/vnd\\.git-lfs\\+json(;|\z))';
$vcs = null;
if ($request->getExists('service')) {
$service = $request->getStr('service');
// We get this initially for `info/refs`.
// Git also gives us a User-Agent like "git/1.8.2.3".
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
} else if (strncmp($user_agent, 'git/', 4) === 0) {
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
} else if ($content_type == 'application/x-git-upload-pack-request') {
// We get this for `git-upload-pack`.
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
} else if ($content_type == 'application/x-git-receive-pack-request') {
// We get this for `git-receive-pack`.
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
} else if (preg_match($lfs_pattern, $content_type)) {
// This is a Git LFS HTTP API request.
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
$this->isGitLFSRequest = true;
} else if ($request_type == 'git-lfs') {
// This is a Git LFS object content request.
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
$this->isGitLFSRequest = true;
} else if ($request->getExists('cmd')) {
// Mercurial also sends an Accept header like
// "application/mercurial-0.1", and a User-Agent like
// "mercurial/proto-1.0".
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL;
} else {
// Subversion also sends an initial OPTIONS request (vs GET/POST), and
// has a User-Agent like "SVN/1.8.3 (x86_64-apple-darwin11.4.2)
// serf/1.3.2".
$dav = $request->getHTTPHeader('DAV');
$dav = new PhutilURI($dav);
if ($dav->getDomain() === 'subversion.tigris.org') {
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN;
}
}
return $vcs;
}
public function handleRequest(AphrontRequest $request) {
$service_exception = null;
$response = null;
try {
$response = $this->serveRequest($request);
} catch (Exception $ex) {
$service_exception = $ex;
}
try {
$remote_addr = $request->getRemoteAddress();
if ($request->isHTTPS()) {
$remote_protocol = PhabricatorRepositoryPullEvent::PROTOCOL_HTTPS;
} else {
$remote_protocol = PhabricatorRepositoryPullEvent::PROTOCOL_HTTP;
}
$pull_event = id(new PhabricatorRepositoryPullEvent())
->setEpoch(PhabricatorTime::getNow())
->setRemoteAddress($remote_addr)
->setRemoteProtocol($remote_protocol);
if ($response) {
$response_code = $response->getHTTPResponseCode();
if ($response_code == 200) {
$pull_event
->setResultType(PhabricatorRepositoryPullEvent::RESULT_PULL)
->setResultCode($response_code);
} else {
$pull_event
->setResultType(PhabricatorRepositoryPullEvent::RESULT_ERROR)
->setResultCode($response_code);
}
if ($response instanceof PhabricatorVCSResponse) {
$pull_event->setProperties(
array(
'response.message' => $response->getMessage(),
));
}
} else {
$pull_event
->setResultType(PhabricatorRepositoryPullEvent::RESULT_EXCEPTION)
->setResultCode(500)
->setProperties(
array(
'exception.class' => get_class($ex),
'exception.message' => $ex->getMessage(),
));
}
$viewer = $this->getServiceViewer();
if ($viewer) {
$pull_event->setPullerPHID($viewer->getPHID());
}
$repository = $this->getServiceRepository();
if ($repository) {
$pull_event->setRepositoryPHID($repository->getPHID());
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$pull_event->save();
unset($unguarded);
} catch (Exception $ex) {
if ($service_exception) {
throw $service_exception;
}
throw $ex;
}
if ($service_exception) {
throw $service_exception;
}
return $response;
}
private function serveRequest(AphrontRequest $request) {
$identifier = $this->getRepositoryIdentifierFromRequest($request);
// If authentication credentials have been provided, try to find a user
// that actually matches those credentials.
// We require both the username and password to be nonempty, because Git
// won't prompt users who provide a username but no password otherwise.
// See T10797 for discussion.
$have_user = strlen(idx($_SERVER, 'PHP_AUTH_USER'));
$have_pass = strlen(idx($_SERVER, 'PHP_AUTH_PW'));
if ($have_user && $have_pass) {
$username = $_SERVER['PHP_AUTH_USER'];
$password = new PhutilOpaqueEnvelope($_SERVER['PHP_AUTH_PW']);
// Try Git LFS auth first since we can usually reject it without doing
// any queries, since the username won't match the one we expect or the
// request won't be LFS.
- $viewer = $this->authenticateGitLFSUser($username, $password);
+ $viewer = $this->authenticateGitLFSUser(
+ $username,
+ $password,
+ $identifier);
// If that failed, try normal auth. Note that we can use normal auth on
// LFS requests, so this isn't strictly an alternative to LFS auth.
if (!$viewer) {
$viewer = $this->authenticateHTTPRepositoryUser($username, $password);
}
if (!$viewer) {
return new PhabricatorVCSResponse(
403,
pht('Invalid credentials.'));
}
} else {
// User hasn't provided credentials, which means we count them as
// being "not logged in".
$viewer = new PhabricatorUser();
}
$this->setServiceViewer($viewer);
$allow_public = PhabricatorEnv::getEnvConfig('policy.allow-public');
$allow_auth = PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth');
if (!$allow_public) {
if (!$viewer->isLoggedIn()) {
if ($allow_auth) {
return new PhabricatorVCSResponse(
401,
pht('You must log in to access repositories.'));
} else {
return new PhabricatorVCSResponse(
403,
pht('Public and authenticated HTTP access are both forbidden.'));
}
}
}
try {
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withIdentifiers(array($identifier))
->needURIs(true)
->executeOne();
if (!$repository) {
return new PhabricatorVCSResponse(
404,
pht('No such repository exists.'));
}
} catch (PhabricatorPolicyException $ex) {
if ($viewer->isLoggedIn()) {
return new PhabricatorVCSResponse(
403,
pht('You do not have permission to access this repository.'));
} else {
if ($allow_auth) {
return new PhabricatorVCSResponse(
401,
pht('You must log in to access this repository.'));
} else {
return new PhabricatorVCSResponse(
403,
pht(
'This repository requires authentication, which is forbidden '.
'over HTTP.'));
}
}
}
$response = $this->validateGitLFSRequest($repository, $viewer);
if ($response) {
return $response;
}
$this->setServiceRepository($repository);
if (!$repository->isTracked()) {
return new PhabricatorVCSResponse(
403,
pht('This repository is inactive.'));
}
$is_push = !$this->isReadOnlyRequest($repository);
if ($this->getIsGitLFSRequest() && $this->getGitLFSToken()) {
// We allow git LFS requests over HTTP even if the repository does not
// otherwise support HTTP reads or writes, as long as the user is using a
// token from SSH. If they're using HTTP username + password auth, they
// have to obey the normal HTTP rules.
} else {
// For now, we don't distinguish between HTTP and HTTPS-originated
// requests that are proxied within the cluster, so the user can connect
// with HTTPS but we may be on HTTP by the time we reach this part of
// the code. Allow things to move forward as long as either protocol
// can be served.
$proto_https = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTPS;
$proto_http = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTP;
$can_read =
$repository->canServeProtocol($proto_https, false) ||
$repository->canServeProtocol($proto_http, false);
if (!$can_read) {
return new PhabricatorVCSResponse(
403,
pht('This repository is not available over HTTP.'));
}
if ($is_push) {
$can_write =
$repository->canServeProtocol($proto_https, true) ||
$repository->canServeProtocol($proto_http, true);
if (!$can_write) {
return new PhabricatorVCSResponse(
403,
pht('This repository is read-only over HTTP.'));
}
}
}
if ($is_push) {
$can_push = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
DiffusionPushCapability::CAPABILITY);
if (!$can_push) {
if ($viewer->isLoggedIn()) {
$error_code = 403;
$error_message = pht(
'You do not have permission to push to this repository ("%s").',
$repository->getDisplayName());
if ($this->getIsGitLFSRequest()) {
return DiffusionGitLFSResponse::newErrorResponse(
$error_code,
$error_message);
} else {
return new PhabricatorVCSResponse(
$error_code,
$error_message);
}
} else {
if ($allow_auth) {
return new PhabricatorVCSResponse(
401,
pht('You must log in to push to this repository.'));
} else {
return new PhabricatorVCSResponse(
403,
pht(
'Pushing to this repository requires authentication, '.
'which is forbidden over HTTP.'));
}
}
}
}
$vcs_type = $repository->getVersionControlSystem();
$req_type = $this->isVCSRequest($request);
if ($vcs_type != $req_type) {
switch ($req_type) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$result = new PhabricatorVCSResponse(
500,
pht(
'This repository ("%s") is not a Git repository.',
$repository->getDisplayName()));
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$result = new PhabricatorVCSResponse(
500,
pht(
'This repository ("%s") is not a Mercurial repository.',
$repository->getDisplayName()));
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$result = new PhabricatorVCSResponse(
500,
pht(
'This repository ("%s") is not a Subversion repository.',
$repository->getDisplayName()));
break;
default:
$result = new PhabricatorVCSResponse(
500,
pht('Unknown request type.'));
break;
}
} else {
switch ($vcs_type) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$result = $this->serveVCSRequest($repository, $viewer);
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$result = new PhabricatorVCSResponse(
500,
pht(
'Phabricator does not support HTTP access to Subversion '.
'repositories.'));
break;
default:
$result = new PhabricatorVCSResponse(
500,
pht('Unknown version control system.'));
break;
}
}
$code = $result->getHTTPResponseCode();
if ($is_push && ($code == 200)) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$repository->writeStatusMessage(
PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
PhabricatorRepositoryStatusMessage::CODE_OKAY);
unset($unguarded);
}
return $result;
}
private function serveVCSRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer) {
// We can serve Git LFS requests first, since we don't need to proxy them.
// It's also important that LFS requests never fall through to standard
// service pathways, because that would let you use LFS tokens to read
// normal repository data.
if ($this->getIsGitLFSRequest()) {
return $this->serveGitLFSRequest($repository, $viewer);
}
// If this repository is hosted on a service, we need to proxy the request
// to a host which can serve it.
$is_cluster_request = $this->getRequest()->isProxiedClusterRequest();
$uri = $repository->getAlmanacServiceURI(
$viewer,
array(
'neverProxy' => $is_cluster_request,
'protocols' => array(
'http',
'https',
),
'writable' => !$this->isReadOnlyRequest($repository),
));
if ($uri) {
$future = $this->getRequest()->newClusterProxyFuture($uri);
return id(new AphrontHTTPProxyResponse())
->setHTTPFuture($future);
}
// Otherwise, we're going to handle the request locally.
$vcs_type = $repository->getVersionControlSystem();
switch ($vcs_type) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$result = $this->serveGitRequest($repository, $viewer);
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$result = $this->serveMercurialRequest($repository, $viewer);
break;
}
return $result;
}
private function isReadOnlyRequest(
PhabricatorRepository $repository) {
$request = $this->getRequest();
$method = $_SERVER['REQUEST_METHOD'];
// TODO: This implementation is safe by default, but very incomplete.
if ($this->getIsGitLFSRequest()) {
return $this->isGitLFSReadOnlyRequest($repository);
}
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$service = $request->getStr('service');
$path = $this->getRequestDirectoryPath($repository);
// NOTE: Service names are the reverse of what you might expect, as they
// are from the point of view of the server. The main read service is
// "git-upload-pack", and the main write service is "git-receive-pack".
if ($method == 'GET' &&
$path == '/info/refs' &&
$service == 'git-upload-pack') {
return true;
}
if ($path == '/git-upload-pack') {
return true;
}
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$cmd = $request->getStr('cmd');
if ($cmd == 'batch') {
$cmds = idx($this->getMercurialArguments(), 'cmds');
return DiffusionMercurialWireProtocol::isReadOnlyBatchCommand($cmds);
}
return DiffusionMercurialWireProtocol::isReadOnlyCommand($cmd);
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
break;
}
return false;
}
/**
* @phutil-external-symbol class PhabricatorStartup
*/
private function serveGitRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer) {
$request = $this->getRequest();
$request_path = $this->getRequestDirectoryPath($repository);
$repository_root = $repository->getLocalPath();
// Rebuild the query string to strip `__magic__` parameters and prevent
// issues where we might interpret inputs like "service=read&service=write"
// differently than the server does and pass it an unsafe command.
// NOTE: This does not use getPassthroughRequestParameters() because
// that code is HTTP-method agnostic and will encode POST data.
$query_data = $_GET;
foreach ($query_data as $key => $value) {
if (!strncmp($key, '__', 2)) {
unset($query_data[$key]);
}
}
$query_string = phutil_build_http_querystring($query_data);
// We're about to wipe out PATH with the rest of the environment, so
// resolve the binary first.
$bin = Filesystem::resolveBinary('git-http-backend');
if (!$bin) {
throw new Exception(
pht(
'Unable to find `%s` in %s!',
'git-http-backend',
'$PATH'));
}
// NOTE: We do not set HTTP_CONTENT_ENCODING here, because we already
// decompressed the request when we read the request body, so the body is
// just plain data with no encoding.
$env = array(
'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'],
'QUERY_STRING' => $query_string,
'CONTENT_TYPE' => $request->getHTTPHeader('Content-Type'),
'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'],
'GIT_PROJECT_ROOT' => $repository_root,
'GIT_HTTP_EXPORT_ALL' => '1',
'PATH_INFO' => $request_path,
'REMOTE_USER' => $viewer->getUsername(),
// TODO: Set these correctly.
// GIT_COMMITTER_NAME
// GIT_COMMITTER_EMAIL
) + $this->getCommonEnvironment($viewer);
$input = PhabricatorStartup::getRawInput();
$command = csprintf('%s', $bin);
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$cluster_engine = id(new DiffusionRepositoryClusterEngine())
->setViewer($viewer)
->setRepository($repository);
$did_write_lock = false;
if ($this->isReadOnlyRequest($repository)) {
$cluster_engine->synchronizeWorkingCopyBeforeRead();
} else {
$did_write_lock = true;
$cluster_engine->synchronizeWorkingCopyBeforeWrite();
}
$caught = null;
try {
list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command))
->setEnv($env, true)
->write($input)
->resolve();
} catch (Exception $ex) {
$caught = $ex;
}
if ($did_write_lock) {
$cluster_engine->synchronizeWorkingCopyAfterWrite();
}
unset($unguarded);
if ($caught) {
throw $caught;
}
if ($err) {
if ($this->isValidGitShallowCloneResponse($stdout, $stderr)) {
// Ignore the error if the response passes this special check for
// validity.
$err = 0;
}
}
if ($err) {
return new PhabricatorVCSResponse(
500,
pht(
'Error %d: %s',
$err,
phutil_utf8ize($stderr)));
}
return id(new DiffusionGitResponse())->setGitData($stdout);
}
private function getRequestDirectoryPath(PhabricatorRepository $repository) {
$request = $this->getRequest();
$request_path = $request->getRequestURI()->getPath();
$info = PhabricatorRepository::parseRepositoryServicePath(
$request_path,
$repository->getVersionControlSystem());
$base_path = $info['path'];
// For Git repositories, strip an optional directory component if it
// isn't the name of a known Git resource. This allows users to clone
// repositories as "/diffusion/X/anything.git", for example.
if ($repository->isGit()) {
$known = array(
'info',
'git-upload-pack',
'git-receive-pack',
);
foreach ($known as $key => $path) {
$known[$key] = preg_quote($path, '@');
}
$known = implode('|', $known);
if (preg_match('@^/([^/]+)/('.$known.')(/|$)@', $base_path)) {
$base_path = preg_replace('@^/([^/]+)@', '', $base_path);
}
}
return $base_path;
}
private function authenticateGitLFSUser(
$username,
- PhutilOpaqueEnvelope $password) {
+ PhutilOpaqueEnvelope $password,
+ $identifier) {
// Never accept these credentials for requests which aren't LFS requests.
if (!$this->getIsGitLFSRequest()) {
return null;
}
// If we have the wrong username, don't bother checking if the token
// is right.
if ($username !== DiffusionGitLFSTemporaryTokenType::HTTP_USERNAME) {
return null;
}
+ // See PHI1123. We need to be able to constrain the token query with
+ // "withTokenResources(...)" to take advantage of the key on the table.
+ // In this case, the repository PHID is the "resource" we're after.
+
+ // In normal workflows, we figure out the viewer first, then use the
+ // viewer to load the repository, but that won't work here. Load the
+ // repository as the omnipotent viewer, then use the repository PHID to
+ // look for a token.
+
+ $omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
+
+ $repository = id(new PhabricatorRepositoryQuery())
+ ->setViewer($omnipotent_viewer)
+ ->withIdentifiers(array($identifier))
+ ->executeOne();
+ if (!$repository) {
+ return null;
+ }
+
$lfs_pass = $password->openEnvelope();
$lfs_hash = PhabricatorHash::weakDigest($lfs_pass);
$token = id(new PhabricatorAuthTemporaryTokenQuery())
- ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->setViewer($omnipotent_viewer)
+ ->withTokenResources(array($repository->getPHID()))
->withTokenTypes(array(DiffusionGitLFSTemporaryTokenType::TOKENTYPE))
->withTokenCodes(array($lfs_hash))
->withExpired(false)
->executeOne();
if (!$token) {
return null;
}
$user = id(new PhabricatorPeopleQuery())
- ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->setViewer($omnipotent_viewer)
->withPHIDs(array($token->getUserPHID()))
->executeOne();
if (!$user) {
return null;
}
if (!$user->isUserActivated()) {
return null;
}
$this->gitLFSToken = $token;
return $user;
}
private function authenticateHTTPRepositoryUser(
$username,
PhutilOpaqueEnvelope $password) {
if (!PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth')) {
// No HTTP auth permitted.
return null;
}
if (!strlen($username)) {
// No username.
return null;
}
if (!strlen($password->openEnvelope())) {
// No password.
return null;
}
$user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUsernames(array($username))
->executeOne();
if (!$user) {
// Username doesn't match anything.
return null;
}
if (!$user->isUserActivated()) {
// User is not activated.
return null;
}
$request = $this->getRequest();
$content_source = PhabricatorContentSource::newFromRequest($request);
$engine = id(new PhabricatorAuthPasswordEngine())
->setViewer($user)
->setContentSource($content_source)
->setPasswordType(PhabricatorAuthPassword::PASSWORD_TYPE_VCS)
->setObject($user);
if (!$engine->isValidPassword($password)) {
return null;
}
return $user;
}
private function serveMercurialRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer) {
$request = $this->getRequest();
$bin = Filesystem::resolveBinary('hg');
if (!$bin) {
throw new Exception(
pht(
'Unable to find `%s` in %s!',
'hg',
'$PATH'));
}
$env = $this->getCommonEnvironment($viewer);
$input = PhabricatorStartup::getRawInput();
$cmd = $request->getStr('cmd');
$args = $this->getMercurialArguments();
$args = $this->formatMercurialArguments($cmd, $args);
if (strlen($input)) {
$input = strlen($input)."\n".$input."0\n";
}
$command = csprintf(
'%s -R %s serve --stdio',
$bin,
$repository->getLocalPath());
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command))
->setEnv($env, true)
->setCWD($repository->getLocalPath())
->write("{$cmd}\n{$args}{$input}")
->resolve();
if ($err) {
return new PhabricatorVCSResponse(
500,
pht('Error %d: %s', $err, $stderr));
}
if ($cmd == 'getbundle' ||
$cmd == 'changegroup' ||
$cmd == 'changegroupsubset') {
// We're not completely sure that "changegroup" and "changegroupsubset"
// actually work, they're for very old Mercurial.
$body = gzcompress($stdout);
} else if ($cmd == 'unbundle') {
// This includes diagnostic information and anything echoed by commit
// hooks. We ignore `stdout` since it just has protocol garbage, and
// substitute `stderr`.
$body = strlen($stderr)."\n".$stderr;
} else {
list($length, $body) = explode("\n", $stdout, 2);
if ($cmd == 'capabilities') {
$body = DiffusionMercurialWireProtocol::filterBundle2Capability($body);
}
}
return id(new DiffusionMercurialResponse())->setContent($body);
}
private function getMercurialArguments() {
// Mercurial sends arguments in HTTP headers. "Why?", you might wonder,
// "Why would you do this?".
$args_raw = array();
for ($ii = 1;; $ii++) {
$header = 'HTTP_X_HGARG_'.$ii;
if (!array_key_exists($header, $_SERVER)) {
break;
}
$args_raw[] = $_SERVER[$header];
}
$args_raw = implode('', $args_raw);
return id(new PhutilQueryStringParser())
->parseQueryString($args_raw);
}
private function formatMercurialArguments($command, array $arguments) {
$spec = DiffusionMercurialWireProtocol::getCommandArgs($command);
$out = array();
// Mercurial takes normal arguments like this:
//
// name <length(value)>
// value
$has_star = false;
foreach ($spec as $arg_key) {
if ($arg_key == '*') {
$has_star = true;
continue;
}
if (isset($arguments[$arg_key])) {
$value = $arguments[$arg_key];
$size = strlen($value);
$out[] = "{$arg_key} {$size}\n{$value}";
unset($arguments[$arg_key]);
}
}
if ($has_star) {
// Mercurial takes arguments for variable argument lists roughly like
// this:
//
// * <count(args)>
// argname1 <length(argvalue1)>
// argvalue1
// argname2 <length(argvalue2)>
// argvalue2
$count = count($arguments);
$out[] = "* {$count}\n";
foreach ($arguments as $key => $value) {
if (in_array($key, $spec)) {
// We already added this argument above, so skip it.
continue;
}
$size = strlen($value);
$out[] = "{$key} {$size}\n{$value}";
}
}
return implode('', $out);
}
private function isValidGitShallowCloneResponse($stdout, $stderr) {
// If you execute `git clone --depth N ...`, git sends a request which
// `git-http-backend` responds to by emitting valid output and then exiting
// with a failure code and an error message. If we ignore this error,
// everything works.
// This is a pretty funky fix: it would be nice to more precisely detect
// that a request is a `--depth N` clone request, but we don't have any code
// to decode protocol frames yet. Instead, look for reasonable evidence
// in the error and output that we're looking at a `--depth` clone.
// For evidence this isn't completely crazy, see:
// https://github.com/schacon/grack/pull/7
$stdout_regexp = '(^Content-Type: application/x-git-upload-pack-result)m';
$stderr_regexp = '(The remote end hung up unexpectedly)';
$has_pack = preg_match($stdout_regexp, $stdout);
$is_hangup = preg_match($stderr_regexp, $stderr);
return $has_pack && $is_hangup;
}
private function getCommonEnvironment(PhabricatorUser $viewer) {
$remote_address = $this->getRequest()->getRemoteAddress();
return array(
DiffusionCommitHookEngine::ENV_USER => $viewer->getUsername(),
DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS => $remote_address,
DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'http',
);
}
private function validateGitLFSRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer) {
if (!$this->getIsGitLFSRequest()) {
return null;
}
if (!$repository->canUseGitLFS()) {
return new PhabricatorVCSResponse(
403,
pht(
'The requested repository ("%s") does not support Git LFS.',
$repository->getDisplayName()));
}
// If this is using an LFS token, sanity check that we're using it on the
// correct repository. This shouldn't really matter since the user could
// just request a proper token anyway, but it suspicious and should not
// be permitted.
$token = $this->getGitLFSToken();
if ($token) {
$resource = $token->getTokenResource();
if ($resource !== $repository->getPHID()) {
return new PhabricatorVCSResponse(
403,
pht(
'The authentication token provided in the request is bound to '.
'a different repository than the requested repository ("%s").',
$repository->getDisplayName()));
}
}
return null;
}
private function serveGitLFSRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer) {
if (!$this->getIsGitLFSRequest()) {
throw new Exception(pht('This is not a Git LFS request!'));
}
$path = $this->getGitLFSRequestPath($repository);
$matches = null;
if (preg_match('(^upload/(.*)\z)', $path, $matches)) {
$oid = $matches[1];
return $this->serveGitLFSUploadRequest($repository, $viewer, $oid);
} else if ($path == 'objects/batch') {
return $this->serveGitLFSBatchRequest($repository, $viewer);
} else {
return DiffusionGitLFSResponse::newErrorResponse(
404,
pht(
'Git LFS operation "%s" is not supported by this server.',
$path));
}
}
private function serveGitLFSBatchRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer) {
$input = $this->getGitLFSInput();
$operation = idx($input, 'operation');
switch ($operation) {
case 'upload':
$want_upload = true;
break;
case 'download':
$want_upload = false;
break;
default:
return DiffusionGitLFSResponse::newErrorResponse(
404,
pht(
'Git LFS batch operation "%s" is not supported by this server.',
$operation));
}
$objects = idx($input, 'objects', array());
$hashes = array();
foreach ($objects as $object) {
$hashes[] = idx($object, 'oid');
}
if ($hashes) {
$refs = id(new PhabricatorRepositoryGitLFSRefQuery())
->setViewer($viewer)
->withRepositoryPHIDs(array($repository->getPHID()))
->withObjectHashes($hashes)
->execute();
$refs = mpull($refs, null, 'getObjectHash');
} else {
$refs = array();
}
$file_phids = mpull($refs, 'getFilePHID');
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
} else {
$files = array();
}
$authorization = null;
$output = array();
foreach ($objects as $object) {
$oid = idx($object, 'oid');
$size = idx($object, 'size');
$ref = idx($refs, $oid);
$error = null;
// NOTE: If we already have a ref for this object, we only emit a
// "download" action. The client should not upload the file again.
$actions = array();
if ($ref) {
$file = idx($files, $ref->getFilePHID());
if ($file) {
// Git LFS may prompt users for authentication if the action does
// not provide an "Authorization" header and does not have a query
// parameter named "token". See here for discussion:
// <https://github.com/github/git-lfs/issues/1088>
$no_authorization = 'Basic '.base64_encode('none');
$get_uri = $file->getCDNURI('data');
$actions['download'] = array(
'href' => $get_uri,
'header' => array(
'Authorization' => $no_authorization,
'X-Phabricator-Request-Type' => 'git-lfs',
),
);
} else {
$error = array(
'code' => 404,
'message' => pht(
'Object "%s" was previously uploaded, but no longer exists '.
'on this server.',
$oid),
);
}
} else if ($want_upload) {
if (!$authorization) {
// Here, we could reuse the existing authorization if we have one,
// but it's a little simpler to just generate a new one
// unconditionally.
$authorization = $this->newGitLFSHTTPAuthorization(
$repository,
$viewer,
$operation);
}
$put_uri = $repository->getGitLFSURI("info/lfs/upload/{$oid}");
$actions['upload'] = array(
'href' => $put_uri,
'header' => array(
'Authorization' => $authorization,
'X-Phabricator-Request-Type' => 'git-lfs',
),
);
}
$object = array(
'oid' => $oid,
'size' => $size,
);
if ($actions) {
$object['actions'] = $actions;
}
if ($error) {
$object['error'] = $error;
}
$output[] = $object;
}
$output = array(
'objects' => $output,
);
return id(new DiffusionGitLFSResponse())
->setContent($output);
}
private function serveGitLFSUploadRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer,
$oid) {
$ref = id(new PhabricatorRepositoryGitLFSRefQuery())
->setViewer($viewer)
->withRepositoryPHIDs(array($repository->getPHID()))
->withObjectHashes(array($oid))
->executeOne();
if ($ref) {
return DiffusionGitLFSResponse::newErrorResponse(
405,
pht(
'Content for object "%s" is already known to this server. It can '.
'not be uploaded again.',
$oid));
}
// Remove the execution time limit because uploading large files may take
// a while.
set_time_limit(0);
$request_stream = new AphrontRequestStream();
$request_iterator = $request_stream->getIterator();
$hashing_iterator = id(new PhutilHashingIterator($request_iterator))
->setAlgorithm('sha256');
$source = id(new PhabricatorIteratorFileUploadSource())
->setName('lfs-'.$oid)
->setViewPolicy(PhabricatorPolicies::POLICY_NOONE)
->setIterator($hashing_iterator);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = $source->uploadFile();
unset($unguarded);
$hash = $hashing_iterator->getHash();
if ($hash !== $oid) {
return DiffusionGitLFSResponse::newErrorResponse(
400,
pht(
'Uploaded data is corrupt or invalid. Expected hash "%s", actual '.
'hash "%s".',
$oid,
$hash));
}
$ref = id(new PhabricatorRepositoryGitLFSRef())
->setRepositoryPHID($repository->getPHID())
->setObjectHash($hash)
->setByteSize($file->getByteSize())
->setAuthorPHID($viewer->getPHID())
->setFilePHID($file->getPHID());
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
// Attach the file to the repository to give users permission
// to access it.
$file->attachToObject($repository->getPHID());
$ref->save();
unset($unguarded);
// This is just a plain HTTP 200 with no content, which is what `git lfs`
// expects.
return new DiffusionGitLFSResponse();
}
private function newGitLFSHTTPAuthorization(
PhabricatorRepository $repository,
PhabricatorUser $viewer,
$operation) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$authorization = DiffusionGitLFSTemporaryTokenType::newHTTPAuthorization(
$repository,
$viewer,
$operation);
unset($unguarded);
return $authorization;
}
private function getGitLFSRequestPath(PhabricatorRepository $repository) {
$request_path = $this->getRequestDirectoryPath($repository);
$matches = null;
if (preg_match('(^/info/lfs(?:\z|/)(.*))', $request_path, $matches)) {
return $matches[1];
}
return null;
}
private function getGitLFSInput() {
if (!$this->gitLFSInput) {
$input = PhabricatorStartup::getRawInput();
$input = phutil_json_decode($input);
$this->gitLFSInput = $input;
}
return $this->gitLFSInput;
}
private function isGitLFSReadOnlyRequest(PhabricatorRepository $repository) {
if (!$this->getIsGitLFSRequest()) {
return false;
}
$path = $this->getGitLFSRequestPath($repository);
if ($path === 'objects/batch') {
$input = $this->getGitLFSInput();
$operation = idx($input, 'operation');
switch ($operation) {
case 'download':
return true;
default:
return false;
}
}
return false;
}
}
diff --git a/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php
index f2a57f7dd..43b8dbb03 100644
--- a/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php
+++ b/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php
@@ -1,728 +1,730 @@
<?php
final class DiffusionRepositoryBasicsManagementPanel
extends DiffusionRepositoryManagementPanel {
const PANELKEY = 'basics';
// c4science customization
public function allowAccess() {
return true;
}
public function getManagementPanelLabel() {
return pht('Basics');
}
public function getManagementPanelOrder() {
return 100;
}
public function getManagementPanelIcon() {
return 'fa-code';
}
protected function getEditEngineFieldKeys() {
return array(
'name',
'callsign',
'shortName',
'description',
'projectPHIDs',
);
}
public function buildManagementPanelCurtain() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$action_list = id(new PhabricatorActionListView())
->setViewer($viewer);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
PhabricatorPolicyCapability::CAN_EDIT);
$edit_uri = $this->getEditPageURI();
$activate_uri = $repository->getPathURI('edit/activate/');
$delete_uri = $repository->getPathURI('edit/delete/');
$encoding_uri = $this->getEditPageURI('encoding');
$dangerous_uri = $repository->getPathURI('edit/dangerous/');
$enormous_uri = $repository->getPathURI('edit/enormous/');
$update_uri = $repository->getPathURI('edit/update/');
if ($repository->isTracked()) {
$activate_icon = 'fa-ban';
$activate_label = pht('Deactivate Repository');
} else {
$activate_icon = 'fa-check';
$activate_label = pht('Activate Repository');
}
$should_dangerous = $repository->shouldAllowDangerousChanges();
if ($should_dangerous) {
$dangerous_icon = 'fa-shield';
$dangerous_name = pht('Prevent Dangerous Changes');
$can_dangerous = $can_edit;
} else {
$dangerous_icon = 'fa-exclamation-triangle';
$dangerous_name = pht('Allow Dangerous Changes');
$can_dangerous = ($can_edit && $repository->canAllowDangerousChanges());
}
$should_enormous = $repository->shouldAllowEnormousChanges();
if ($should_enormous) {
$enormous_icon = 'fa-shield';
$enormous_name = pht('Prevent Enormous Changes');
$can_enormous = $can_edit;
} else {
$enormous_icon = 'fa-exclamation-triangle';
$enormous_name = pht('Allow Enormous Changes');
$can_enormous = ($can_edit && $repository->canAllowEnormousChanges());
}
$action_list->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Basic Information'))
->setHref($edit_uri)
->setIcon('fa-pencil')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$action_list->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Text Encoding'))
->setIcon('fa-text-width')
->setHref($encoding_uri)
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$action_list->addAction(
id(new PhabricatorActionView())
->setName($dangerous_name)
->setHref($dangerous_uri)
->setIcon($dangerous_icon)
->setDisabled(!$can_dangerous)
->setWorkflow(true));
$action_list->addAction(
id(new PhabricatorActionView())
->setName($enormous_name)
->setHref($enormous_uri)
->setIcon($enormous_icon)
->setDisabled(!$can_enormous)
->setWorkflow(true));
$action_list->addAction(
id(new PhabricatorActionView())
->setName($activate_label)
->setHref($activate_uri)
->setIcon($activate_icon)
->setDisabled(!$can_edit)
->setWorkflow(true));
$action_list->addAction(
id(new PhabricatorActionView())
->setName(pht('Update Now'))
->setHref($update_uri)
->setIcon('fa-refresh')
->setWorkflow(true)
->setDisabled(!$can_edit));
// c4science customization
// $action_list->addAction(
// id(new PhabricatorActionView())
// ->setType(PhabricatorActionView::TYPE_DIVIDER));
// c4science customization
// $action_list->addAction(
// id(new PhabricatorActionView())
// ->setName(pht('Delete Repository'))
// ->setHref($delete_uri)
// ->setIcon('fa-times')
// ->setColor(PhabricatorActionView::RED)
// ->setDisabled(true)
// ->setWorkflow(true));
return $this->newCurtainView()
->setActionList($action_list);
}
public function buildManagementPanelContent() {
$basics = $this->buildBasics();
$basics = $this->newBox(pht('Properties'), $basics);
$repository = $this->getRepository();
$is_new = $repository->isNewlyInitialized();
$info_view = null;
if ($is_new) {
$messages = array();
$messages[] = pht(
'This newly created repository is not active yet. Configure policies, '.
'options, and URIs. When ready, %s the repository.',
phutil_tag('strong', array(), pht('Activate')));
if ($repository->isHosted()) {
$messages[] = pht(
'If activated now, this repository will become a new hosted '.
'repository. To observe an existing repository instead, configure '.
'it in the %s panel.',
phutil_tag('strong', array(), pht('URIs')));
} else {
$messages[] = pht(
'If activated now, this repository will observe an existing remote '.
'repository and begin importing changes.');
}
$info_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setErrors($messages);
}
$description = $this->buildDescription();
if ($description) {
$description = $this->newBox(pht('Description'), $description);
}
$status = $this->buildStatus();
return array($info_view, $basics, $description, $status);
}
private function buildBasics() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setViewer($viewer);
$name = $repository->getName();
$view->addProperty(pht('Name'), $name);
$type = PhabricatorRepositoryType::getNameForRepositoryType(
$repository->getVersionControlSystem());
$view->addProperty(pht('Type'), $type);
$callsign = $repository->getCallsign();
if (!strlen($callsign)) {
$callsign = phutil_tag('em', array(), pht('No Callsign'));
}
$view->addProperty(pht('Callsign'), $callsign);
$short_name = $repository->getRepositorySlug();
if ($short_name === null) {
$short_name = phutil_tag('em', array(), pht('No Short Name'));
}
$view->addProperty(pht('Short Name'), $short_name);
$encoding = $repository->getDetail('encoding');
if (!$encoding) {
$encoding = phutil_tag('em', array(), pht('Use Default (UTF-8)'));
}
$view->addProperty(pht('Encoding'), $encoding);
$can_dangerous = $repository->canAllowDangerousChanges();
if (!$can_dangerous) {
$dangerous = phutil_tag('em', array(), pht('Not Preventable'));
} else {
$should_dangerous = $repository->shouldAllowDangerousChanges();
if ($should_dangerous) {
$dangerous = pht('Allowed');
} else {
$dangerous = pht('Not Allowed');
}
}
$view->addProperty(pht('Dangerous Changes'), $dangerous);
$can_enormous = $repository->canAllowEnormousChanges();
if (!$can_enormous) {
$enormous = phutil_tag('em', array(), pht('Not Preventable'));
} else {
$should_enormous = $repository->shouldAllowEnormousChanges();
if ($should_enormous) {
$enormous = pht('Allowed');
} else {
$enormous = pht('Not Allowed');
}
}
$view->addProperty(pht('Enormous Changes'), $enormous);
return $view;
}
private function buildDescription() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$description = $repository->getDetail('description');
$view = id(new PHUIPropertyListView())
->setViewer($viewer);
if (!strlen($description)) {
return null;
} else {
$description = new PHUIRemarkupView($viewer, $description);
}
$view->addTextContent($description);
return $view;
}
private function buildStatus() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setViewer($viewer);
$view->addProperty(
pht('Update Frequency'),
$this->buildRepositoryUpdateInterval($repository));
$messages = $this->loadStatusMessages($repository);
$status = $this->buildRepositoryStatus($repository, $messages);
$raw_error = $this->buildRepositoryRawError($repository, $messages);
$view->addProperty(pht('Status'), $status);
if ($raw_error) {
$view->addSectionHeader(pht('Raw Error'));
$view->addTextContent($raw_error);
}
return $this->newBox(pht('Status'), $view);
}
private function buildRepositoryUpdateInterval(
PhabricatorRepository $repository) {
$smart_wait = $repository->loadUpdateInterval();
$doc_href = PhabricatorEnv::getDoclink(
'Diffusion User Guide: Repository Updates');
return array(
phutil_format_relative_time_detailed($smart_wait),
" \xC2\xB7 ",
phutil_tag(
'a',
array(
'href' => $doc_href,
'target' => '_blank',
),
pht('Learn More')),
);
}
private function buildRepositoryStatus(
PhabricatorRepository $repository,
array $messages) {
$viewer = $this->getViewer();
$is_cluster = $repository->getAlmanacServicePHID();
$view = new PHUIStatusListView();
if ($repository->isTracked()) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Repository Active')));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'bluegrey')
->setTarget(pht('Repository Inactive'))
->setNote(
pht('Activate this repository to begin or resume import.')));
return $view;
}
$binaries = array();
$svnlook_check = false;
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$binaries[] = 'git';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$binaries[] = 'svn';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$binaries[] = 'hg';
break;
}
if ($repository->isHosted()) {
$proto_https = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTPS;
$proto_http = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTP;
$can_http = $repository->canServeProtocol($proto_http, false) ||
$repository->canServeProtocol($proto_https, false);
if ($can_http) {
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$binaries[] = 'git-http-backend';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$binaries[] = 'svnserve';
$binaries[] = 'svnadmin';
$binaries[] = 'svnlook';
$svnlook_check = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$binaries[] = 'hg';
break;
}
}
$proto_ssh = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH;
$can_ssh = $repository->canServeProtocol($proto_ssh, false);
if ($can_ssh) {
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$binaries[] = 'git-receive-pack';
$binaries[] = 'git-upload-pack';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$binaries[] = 'svnserve';
$binaries[] = 'svnadmin';
$binaries[] = 'svnlook';
$svnlook_check = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$binaries[] = 'hg';
break;
}
}
}
$binaries = array_unique($binaries);
if (!$is_cluster) {
// We're only checking for binaries if we aren't running with a cluster
// configuration. In theory, we could check for binaries on the
// repository host machine, but we'd need to make this more complicated
// to do that.
foreach ($binaries as $binary) {
$where = Filesystem::resolveBinary($binary);
if (!$where) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(
pht('Missing Binary %s', phutil_tag('tt', array(), $binary)))
->setNote(pht(
"Unable to find this binary in the webserver's PATH. You may ".
"need to configure %s.",
$this->getEnvConfigLink())));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(
pht('Found Binary %s', phutil_tag('tt', array(), $binary)))
->setNote(phutil_tag('tt', array(), $where)));
}
}
// This gets checked generically above. However, for svn commit hooks, we
// need this to be in environment.append-paths because subversion strips
// PATH.
if ($svnlook_check) {
$where = Filesystem::resolveBinary('svnlook');
if ($where) {
$path = substr($where, 0, strlen($where) - strlen('svnlook'));
$dirs = PhabricatorEnv::getEnvConfig('environment.append-paths');
$in_path = false;
foreach ($dirs as $dir) {
if (Filesystem::isDescendant($path, $dir)) {
$in_path = true;
break;
}
}
if (!$in_path) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(
- pht('Missing Binary %s', phutil_tag('tt', array(), $binary)))
- ->setNote(pht(
- 'Unable to find this binary in `%s`. '.
- 'You need to configure %s and include %s.',
- 'environment.append-paths',
- $this->getEnvConfigLink(),
- $path)));
+ pht('Commit Hooks: %s', phutil_tag('tt', array(), $binary)))
+ ->setNote(
+ pht(
+ 'The directory containing the "svnlook" binary is not '.
+ 'listed in "environment.append-paths", so commit hooks '.
+ '(which execute with an empty "PATH") will not be able to '.
+ 'find "svnlook". Add `%s` to %s.',
+ $path,
+ $this->getEnvConfigLink())));
}
}
}
}
$doc_href = PhabricatorEnv::getDoclink('Managing Daemons with phd');
$daemon_instructions = pht(
'Use %s to start daemons. See %s.',
phutil_tag('tt', array(), 'bin/phd start'),
phutil_tag(
'a',
array(
'href' => $doc_href,
),
pht('Managing Daemons with phd')));
$pull_daemon = id(new PhabricatorDaemonLogQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE)
->withDaemonClasses(array('PhabricatorRepositoryPullLocalDaemon'))
->setLimit(1)
->execute();
if ($pull_daemon) {
// TODO: In a cluster environment, we need a daemon on this repository's
// host, specifically, and we aren't checking for that right now. This
// is a reasonable proxy for things being more-or-less correctly set up,
// though.
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Pull Daemon Running')));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('Pull Daemon Not Running'))
->setNote($daemon_instructions));
}
$task_daemon = id(new PhabricatorDaemonLogQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE)
->withDaemonClasses(array('PhabricatorTaskmasterDaemon'))
->setLimit(1)
->execute();
if ($task_daemon) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Task Daemon Running')));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('Task Daemon Not Running'))
->setNote($daemon_instructions));
}
if ($is_cluster) {
// Just omit this status check for now in cluster environments. We
// could make a service call and pull it from the repository host
// eventually.
} else if ($repository->usesLocalWorkingCopy()) {
$local_parent = dirname($repository->getLocalPath());
if (Filesystem::pathExists($local_parent)) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Storage Directory OK'))
->setNote(phutil_tag('tt', array(), $local_parent)));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('No Storage Directory'))
->setNote(
pht(
'Storage directory %s does not exist, or is not readable by '.
'the webserver. Create this directory or make it readable.',
phutil_tag('tt', array(), $local_parent))));
return $view;
}
$local_path = $repository->getLocalPath();
$message = idx($messages, PhabricatorRepositoryStatusMessage::TYPE_INIT);
if ($message) {
switch ($message->getStatusCode()) {
case PhabricatorRepositoryStatusMessage::CODE_ERROR:
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('Initialization Error'))
->setNote($message->getParameter('message')));
return $view;
case PhabricatorRepositoryStatusMessage::CODE_OKAY:
if (Filesystem::pathExists($local_path)) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Working Copy OK'))
->setNote(phutil_tag('tt', array(), $local_path)));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('Working Copy Error'))
->setNote(
pht(
'Working copy %s has been deleted, or is not '.
'readable by the webserver. Make this directory '.
'readable. If it has been deleted, the daemons should '.
'restore it automatically.',
phutil_tag('tt', array(), $local_path))));
return $view;
}
break;
default:
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_CLOCK, 'green')
->setTarget(pht('Initializing Working Copy'))
->setNote(pht('Daemons are initializing the working copy.')));
return $view;
}
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_CLOCK, 'orange')
->setTarget(pht('No Working Copy Yet'))
->setNote(
pht('Waiting for daemons to build a working copy.')));
return $view;
}
}
$message = idx($messages, PhabricatorRepositoryStatusMessage::TYPE_FETCH);
if ($message) {
switch ($message->getStatusCode()) {
case PhabricatorRepositoryStatusMessage::CODE_ERROR:
$message = $message->getParameter('message');
$suggestion = null;
if (preg_match('/Permission denied \(publickey\)./', $message)) {
$suggestion = pht(
'Public Key Error: This error usually indicates that the '.
'keypair you have configured does not have permission to '.
'access the repository.');
}
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('Update Error'))
->setNote($suggestion));
return $view;
case PhabricatorRepositoryStatusMessage::CODE_OKAY:
$ago = (PhabricatorTime::getNow() - $message->getEpoch());
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Updates OK'))
->setNote(
pht(
'Last updated %s (%s ago).',
phabricator_datetime($message->getEpoch(), $viewer),
phutil_format_relative_time_detailed($ago))));
break;
}
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_CLOCK, 'orange')
->setTarget(pht('Waiting For Update'))
->setNote(
pht('Waiting for daemons to read updates.')));
}
if ($repository->isImporting()) {
$ratio = $repository->loadImportProgress();
$percentage = sprintf('%.2f%%', 100 * $ratio);
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_CLOCK, 'green')
->setTarget(pht('Importing'))
->setNote(
pht('%s Complete', $percentage)));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Fully Imported')));
}
if (idx($messages, PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE)) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_UP, 'indigo')
->setTarget(pht('Prioritized'))
->setNote(pht('This repository will be updated soon!')));
}
return $view;
}
private function buildRepositoryRawError(
PhabricatorRepository $repository,
array $messages) {
$viewer = $this->getViewer();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
PhabricatorPolicyCapability::CAN_EDIT);
$raw_error = null;
$message = idx($messages, PhabricatorRepositoryStatusMessage::TYPE_FETCH);
if ($message) {
switch ($message->getStatusCode()) {
case PhabricatorRepositoryStatusMessage::CODE_ERROR:
$raw_error = $message->getParameter('message');
break;
}
}
if ($raw_error !== null) {
if (!$can_edit) {
$raw_message = pht(
'You must be able to edit a repository to see raw error messages '.
'because they sometimes disclose sensitive information.');
$raw_message = phutil_tag('em', array(), $raw_message);
} else {
$raw_message = phutil_escape_html_newlines($raw_error);
}
} else {
$raw_message = null;
}
return $raw_message;
}
private function loadStatusMessages(PhabricatorRepository $repository) {
$messages = id(new PhabricatorRepositoryStatusMessage())
->loadAllWhere('repositoryID = %d', $repository->getID());
$messages = mpull($messages, null, 'getStatusType');
return $messages;
}
private function getEnvConfigLink() {
$config_href = '/config/edit/environment.append-paths/';
return phutil_tag(
'a',
array(
'href' => $config_href,
),
'environment.append-paths');
}
}
diff --git a/src/applications/diffusion/query/DiffusionCommitQuery.php b/src/applications/diffusion/query/DiffusionCommitQuery.php
index 05072e07c..bc555a4fb 100644
--- a/src/applications/diffusion/query/DiffusionCommitQuery.php
+++ b/src/applications/diffusion/query/DiffusionCommitQuery.php
@@ -1,938 +1,974 @@
<?php
final class DiffusionCommitQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $authorPHIDs;
private $defaultRepository;
private $identifiers;
private $repositoryIDs;
private $repositoryPHIDs;
private $identifierMap;
private $responsiblePHIDs;
private $statuses;
private $packagePHIDs;
private $unreachable;
private $needAuditRequests;
private $needAuditAuthority;
private $auditIDs;
private $auditorPHIDs;
private $epochMin;
private $epochMax;
private $importing;
private $ancestorsOf;
private $needCommitData;
private $needDrafts;
private $needIdentities;
private $mustFilterRefs = false;
private $refRepository;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAuthorPHIDs(array $phids) {
$this->authorPHIDs = $phids;
return $this;
}
/**
* Load commits by partial or full identifiers, e.g. "rXab82393", "rX1234",
* or "a9caf12". When an identifier matches multiple commits, they will all
* be returned; callers should be prepared to deal with more results than
* they queried for.
*/
public function withIdentifiers(array $identifiers) {
// Some workflows (like blame lookups) can pass in large numbers of
// duplicate identifiers. We only care about unique identifiers, so
// get rid of duplicates immediately.
$identifiers = array_fuse($identifiers);
$this->identifiers = $identifiers;
return $this;
}
/**
* Look up commits in a specific repository. This is a shorthand for calling
* @{method:withDefaultRepository} and @{method:withRepositoryIDs}.
*/
public function withRepository(PhabricatorRepository $repository) {
$this->withDefaultRepository($repository);
$this->withRepositoryIDs(array($repository->getID()));
return $this;
}
/**
* Look up commits in a specific repository. Prefer
* @{method:withRepositoryIDs}; the underlying table is keyed by ID such
* that this method requires a separate initial query to map PHID to ID.
*/
public function withRepositoryPHIDs(array $phids) {
$this->repositoryPHIDs = $phids;
return $this;
}
/**
* If a default repository is provided, ambiguous commit identifiers will
* be assumed to belong to the default repository.
*
* For example, "r123" appearing in a commit message in repository X is
* likely to be unambiguously "rX123". Normally the reference would be
* considered ambiguous, but if you provide a default repository it will
* be correctly resolved.
*/
public function withDefaultRepository(PhabricatorRepository $repository) {
$this->defaultRepository = $repository;
return $this;
}
public function withRepositoryIDs(array $repository_ids) {
$this->repositoryIDs = array_unique($repository_ids);
return $this;
}
public function needCommitData($need) {
$this->needCommitData = $need;
return $this;
}
public function needDrafts($need) {
$this->needDrafts = $need;
return $this;
}
public function needIdentities($need) {
$this->needIdentities = $need;
return $this;
}
public function needAuditRequests($need) {
$this->needAuditRequests = $need;
return $this;
}
public function needAuditAuthority(array $users) {
assert_instances_of($users, 'PhabricatorUser');
$this->needAuditAuthority = $users;
return $this;
}
public function withAuditIDs(array $ids) {
$this->auditIDs = $ids;
return $this;
}
public function withAuditorPHIDs(array $auditor_phids) {
$this->auditorPHIDs = $auditor_phids;
return $this;
}
public function withResponsiblePHIDs(array $responsible_phids) {
$this->responsiblePHIDs = $responsible_phids;
return $this;
}
public function withPackagePHIDs(array $package_phids) {
$this->packagePHIDs = $package_phids;
return $this;
}
public function withUnreachable($unreachable) {
$this->unreachable = $unreachable;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withEpochRange($min, $max) {
$this->epochMin = $min;
$this->epochMax = $max;
return $this;
}
public function withImporting($importing) {
$this->importing = $importing;
return $this;
}
public function withAncestorsOf(array $refs) {
$this->ancestorsOf = $refs;
return $this;
}
public function getIdentifierMap() {
if ($this->identifierMap === null) {
throw new Exception(
pht(
'You must %s the query before accessing the identifier map.',
'execute()'));
}
return $this->identifierMap;
}
protected function getPrimaryTableAlias() {
return 'commit';
}
protected function willExecute() {
if ($this->identifierMap === null) {
$this->identifierMap = array();
}
}
public function newResultObject() {
return new PhabricatorRepositoryCommit();
}
protected function loadPage() {
$table = $this->newResultObject();
$conn = $table->establishConnection('r');
+ $empty_exception = null;
$subqueries = array();
if ($this->responsiblePHIDs) {
$base_authors = $this->authorPHIDs;
$base_auditors = $this->auditorPHIDs;
$responsible_phids = $this->responsiblePHIDs;
if ($base_authors) {
$all_authors = array_merge($base_authors, $responsible_phids);
} else {
$all_authors = $responsible_phids;
}
if ($base_auditors) {
$all_auditors = array_merge($base_auditors, $responsible_phids);
} else {
$all_auditors = $responsible_phids;
}
$this->authorPHIDs = $all_authors;
$this->auditorPHIDs = $base_auditors;
- $subqueries[] = $this->buildStandardPageQuery(
- $conn,
- $table->getTableName());
+ try {
+ $subqueries[] = $this->buildStandardPageQuery(
+ $conn,
+ $table->getTableName());
+ } catch (PhabricatorEmptyQueryException $ex) {
+ $empty_exception = $ex;
+ }
$this->authorPHIDs = $base_authors;
$this->auditorPHIDs = $all_auditors;
- $subqueries[] = $this->buildStandardPageQuery(
- $conn,
- $table->getTableName());
+ try {
+ $subqueries[] = $this->buildStandardPageQuery(
+ $conn,
+ $table->getTableName());
+ } catch (PhabricatorEmptyQueryException $ex) {
+ $empty_exception = $ex;
+ }
} else {
$subqueries[] = $this->buildStandardPageQuery(
$conn,
$table->getTableName());
}
+ if (!$subqueries) {
+ throw $empty_exception;
+ }
+
if (count($subqueries) > 1) {
$unions = null;
foreach ($subqueries as $subquery) {
if (!$unions) {
$unions = qsprintf(
$conn,
'(%Q)',
$subquery);
continue;
}
$unions = qsprintf(
$conn,
'%Q UNION DISTINCT (%Q)',
$unions,
$subquery);
}
$query = qsprintf(
$conn,
'%Q %Q %Q',
$unions,
$this->buildOrderClause($conn, true),
$this->buildLimitClause($conn));
} else {
$query = head($subqueries);
}
$rows = queryfx_all($conn, '%Q', $query);
$rows = $this->didLoadRawRows($rows);
return $table->loadAllFromArray($rows);
}
protected function willFilterPage(array $commits) {
$repository_ids = mpull($commits, 'getRepositoryID', 'getRepositoryID');
$repos = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withIDs($repository_ids)
->execute();
$min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH;
$result = array();
foreach ($commits as $key => $commit) {
$repo = idx($repos, $commit->getRepositoryID());
if ($repo) {
$commit->attachRepository($repo);
} else {
$this->didRejectResult($commit);
unset($commits[$key]);
continue;
}
// Build the identifierMap
if ($this->identifiers !== null) {
$ids = $this->identifiers;
$prefixes = array(
'r'.$commit->getRepository()->getCallsign(),
'r'.$commit->getRepository()->getCallsign().':',
'R'.$commit->getRepository()->getID().':',
'', // No prefix is valid too and will only match the commitIdentifier
);
$suffix = $commit->getCommitIdentifier();
if ($commit->getRepository()->isSVN()) {
foreach ($prefixes as $prefix) {
if (isset($ids[$prefix.$suffix])) {
$result[$prefix.$suffix][] = $commit;
}
}
} else {
// This awkward construction is so we can link the commits up in O(N)
// time instead of O(N^2).
for ($ii = $min_qualified; $ii <= strlen($suffix); $ii++) {
$part = substr($suffix, 0, $ii);
foreach ($prefixes as $prefix) {
if (isset($ids[$prefix.$part])) {
$result[$prefix.$part][] = $commit;
}
}
}
}
}
}
if ($result) {
foreach ($result as $identifier => $matching_commits) {
if (count($matching_commits) == 1) {
$result[$identifier] = head($matching_commits);
} else {
// This reference is ambiguous (it matches more than one commit) so
// don't link it.
unset($result[$identifier]);
}
}
$this->identifierMap += $result;
}
return $commits;
}
protected function didFilterPage(array $commits) {
$viewer = $this->getViewer();
if ($this->mustFilterRefs) {
// If this flag is set, the query has an "Ancestors Of" constraint and
// at least one of the constraining refs had too many ancestors for us
// to apply the constraint with a big "commitIdentifier IN (%Ls)" clause.
// We're going to filter each page and hope we get a full result set
// before the query overheats.
$ancestor_list = mpull($commits, 'getCommitIdentifier');
$ancestor_list = array_values($ancestor_list);
foreach ($this->ancestorsOf as $ref) {
try {
$ancestor_list = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
DiffusionRequest::newFromDictionary(
array(
'repository' => $this->refRepository,
'user' => $viewer,
)),
'diffusion.internal.ancestors',
array(
'ref' => $ref,
'commits' => $ancestor_list,
));
} catch (ConduitClientException $ex) {
throw new PhabricatorSearchConstraintException(
$ex->getMessage());
}
if (!$ancestor_list) {
break;
}
}
$ancestor_list = array_fuse($ancestor_list);
foreach ($commits as $key => $commit) {
$identifier = $commit->getCommitIdentifier();
if (!isset($ancestor_list[$identifier])) {
$this->didRejectResult($commit);
unset($commits[$key]);
}
}
if (!$commits) {
return $commits;
}
}
if ($this->needCommitData) {
$data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(
'commitID in (%Ld)',
mpull($commits, 'getID'));
$data = mpull($data, null, 'getCommitID');
foreach ($commits as $commit) {
$commit_data = idx($data, $commit->getID());
if (!$commit_data) {
$commit_data = new PhabricatorRepositoryCommitData();
}
$commit->attachCommitData($commit_data);
}
}
if ($this->needAuditRequests) {
$requests = id(new PhabricatorRepositoryAuditRequest())->loadAllWhere(
'commitPHID IN (%Ls)',
mpull($commits, 'getPHID'));
$requests = mgroup($requests, 'getCommitPHID');
foreach ($commits as $commit) {
$audit_requests = idx($requests, $commit->getPHID(), array());
$commit->attachAudits($audit_requests);
foreach ($audit_requests as $audit_request) {
$audit_request->attachCommit($commit);
}
}
}
if ($this->needIdentities) {
$identity_phids = array_merge(
mpull($commits, 'getAuthorIdentityPHID'),
mpull($commits, 'getCommitterIdentityPHID'));
$data = id(new PhabricatorRepositoryIdentityQuery())
->withPHIDs($identity_phids)
->setViewer($this->getViewer())
->execute();
$data = mpull($data, null, 'getPHID');
foreach ($commits as $commit) {
$author_identity = idx($data, $commit->getAuthorIdentityPHID());
$committer_identity = idx($data, $commit->getCommitterIdentityPHID());
$commit->attachIdentities($author_identity, $committer_identity);
}
}
if ($this->needDrafts) {
PhabricatorDraftEngine::attachDrafts(
$viewer,
$commits);
}
if ($this->needAuditAuthority) {
$authority_users = $this->needAuditAuthority;
// NOTE: This isn't very efficient since we're running two queries per
// user, but there's currently no way to figure out authority for
// multiple users in one query. Today, we only ever request authority for
// a single user and single commit, so this has no practical impact.
// NOTE: We're querying with the viewership of query viewer, not the
// actual users. If the viewer can't see a project or package, they
// won't be able to see who has authority on it. This is safer than
// showing them true authority, and should never matter today, but it
// also doesn't seem like a significant disclosure and might be
// reasonable to adjust later if it causes something weird or confusing
// to happen.
$authority_map = array();
foreach ($authority_users as $authority_user) {
$authority_phid = $authority_user->getPHID();
if (!$authority_phid) {
continue;
}
$result_phids = array();
// Users have authority over themselves.
$result_phids[] = $authority_phid;
// Users have authority over packages they own.
$owned_packages = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withAuthorityPHIDs(array($authority_phid))
->execute();
foreach ($owned_packages as $package) {
$result_phids[] = $package->getPHID();
}
// Users have authority over projects they're members of.
$projects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withMemberPHIDs(array($authority_phid))
->execute();
foreach ($projects as $project) {
$result_phids[] = $project->getPHID();
}
$result_phids = array_fuse($result_phids);
foreach ($commits as $commit) {
$attach_phids = $result_phids;
// NOTE: When modifying your own commits, you act only on behalf of
// yourself, not your packages or projects. The idea here is that you
// can't accept your own commits. In the future, this might change or
// depend on configuration.
$author_phid = $commit->getAuthorPHID();
if ($author_phid == $authority_phid) {
$attach_phids = array($author_phid);
$attach_phids = array_fuse($attach_phids);
}
$commit->attachAuditAuthority($authority_user, $attach_phids);
}
}
}
return $commits;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->repositoryPHIDs !== null) {
$map_repositories = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withPHIDs($this->repositoryPHIDs)
->execute();
if (!$map_repositories) {
throw new PhabricatorEmptyQueryException();
}
$repository_ids = mpull($map_repositories, 'getID');
if ($this->repositoryIDs !== null) {
$repository_ids = array_merge($repository_ids, $this->repositoryIDs);
}
$this->withRepositoryIDs($repository_ids);
}
if ($this->ancestorsOf !== null) {
if (count($this->repositoryIDs) !== 1) {
throw new PhabricatorSearchConstraintException(
pht(
'To search for commits which are ancestors of particular refs, '.
'you must constrain the search to exactly one repository.'));
}
$repository_id = head($this->repositoryIDs);
$history_limit = $this->getRawResultLimit() * 32;
$viewer = $this->getViewer();
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withIDs(array($repository_id))
->executeOne();
if (!$repository) {
throw new PhabricatorEmptyQueryException();
}
if ($repository->isSVN()) {
throw new PhabricatorSearchConstraintException(
pht(
'Subversion does not support searching for ancestors of '.
'a particular ref. This operation is not meaningful in '.
'Subversion.'));
}
if ($repository->isHg()) {
throw new PhabricatorSearchConstraintException(
pht(
'Mercurial does not currently support searching for ancestors of '.
'a particular ref.'));
}
$can_constrain = true;
$history_identifiers = array();
foreach ($this->ancestorsOf as $key => $ref) {
try {
$raw_history = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
DiffusionRequest::newFromDictionary(
array(
'repository' => $repository,
'user' => $viewer,
)),
'diffusion.historyquery',
array(
'commit' => $ref,
'limit' => $history_limit,
));
} catch (ConduitClientException $ex) {
throw new PhabricatorSearchConstraintException(
$ex->getMessage());
}
$ref_identifiers = array();
foreach ($raw_history['pathChanges'] as $change) {
$ref_identifiers[] = $change['commitIdentifier'];
}
// If this ref had fewer total commits than the limit, we're safe to
// apply the constraint as a large `IN (...)` query for a list of
// commit identifiers. This is efficient.
if ($history_limit) {
if (count($ref_identifiers) >= $history_limit) {
$can_constrain = false;
break;
}
}
$history_identifiers += array_fuse($ref_identifiers);
}
// If all refs had a small number of ancestors, we can just put the
// constraint into the query here and we're done. Otherwise, we need
// to filter each page after it comes out of the MySQL layer.
if ($can_constrain) {
$where[] = qsprintf(
$conn,
'commit.commitIdentifier IN (%Ls)',
$history_identifiers);
} else {
$this->mustFilterRefs = true;
$this->refRepository = $repository;
}
}
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'commit.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'commit.phid IN (%Ls)',
$this->phids);
}
if ($this->repositoryIDs !== null) {
$where[] = qsprintf(
$conn,
'commit.repositoryID IN (%Ld)',
$this->repositoryIDs);
}
if ($this->authorPHIDs !== null) {
+ $author_phids = $this->authorPHIDs;
+ if ($author_phids) {
+ $author_phids = $this->selectPossibleAuthors($author_phids);
+ if (!$author_phids) {
+ throw new PhabricatorEmptyQueryException(
+ pht('Author PHIDs contain no possible authors.'));
+ }
+ }
+
$where[] = qsprintf(
$conn,
'commit.authorPHID IN (%Ls)',
- $this->authorPHIDs);
+ $author_phids);
}
if ($this->epochMin !== null) {
$where[] = qsprintf(
$conn,
'commit.epoch >= %d',
$this->epochMin);
}
if ($this->epochMax !== null) {
$where[] = qsprintf(
$conn,
'commit.epoch <= %d',
$this->epochMax);
}
if ($this->importing !== null) {
if ($this->importing) {
$where[] = qsprintf(
$conn,
'(commit.importStatus & %d) != %d',
PhabricatorRepositoryCommit::IMPORTED_ALL,
PhabricatorRepositoryCommit::IMPORTED_ALL);
} else {
$where[] = qsprintf(
$conn,
'(commit.importStatus & %d) = %d',
PhabricatorRepositoryCommit::IMPORTED_ALL,
PhabricatorRepositoryCommit::IMPORTED_ALL);
}
}
if ($this->identifiers !== null) {
$min_unqualified = PhabricatorRepository::MINIMUM_UNQUALIFIED_HASH;
$min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH;
$refs = array();
$bare = array();
foreach ($this->identifiers as $identifier) {
$matches = null;
preg_match('/^(?:[rR]([A-Z]+:?|[0-9]+:))?(.*)$/',
$identifier, $matches);
$repo = nonempty(rtrim($matches[1], ':'), null);
$commit_identifier = nonempty($matches[2], null);
if ($repo === null) {
if ($this->defaultRepository) {
$repo = $this->defaultRepository->getPHID();
}
}
if ($repo === null) {
if (strlen($commit_identifier) < $min_unqualified) {
continue;
}
$bare[] = $commit_identifier;
} else {
$refs[] = array(
'repository' => $repo,
'identifier' => $commit_identifier,
);
}
}
$sql = array();
foreach ($bare as $identifier) {
$sql[] = qsprintf(
$conn,
'(commit.commitIdentifier LIKE %> AND '.
'LENGTH(commit.commitIdentifier) = 40)',
$identifier);
}
if ($refs) {
$repositories = ipull($refs, 'repository');
$repos = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withIdentifiers($repositories);
$repos->execute();
$repos = $repos->getIdentifierMap();
foreach ($refs as $key => $ref) {
$repo = idx($repos, $ref['repository']);
if (!$repo) {
continue;
}
if ($repo->isSVN()) {
if (!ctype_digit((string)$ref['identifier'])) {
continue;
}
$sql[] = qsprintf(
$conn,
'(commit.repositoryID = %d AND commit.commitIdentifier = %s)',
$repo->getID(),
// NOTE: Because the 'commitIdentifier' column is a string, MySQL
// ignores the index if we hand it an integer. Hand it a string.
// See T3377.
(int)$ref['identifier']);
} else {
if (strlen($ref['identifier']) < $min_qualified) {
continue;
}
$identifier = $ref['identifier'];
if (strlen($identifier) == 40) {
// MySQL seems to do slightly better with this version if the
// clause, so issue it if we have a full commit hash.
$sql[] = qsprintf(
$conn,
'(commit.repositoryID = %d
AND commit.commitIdentifier = %s)',
$repo->getID(),
$identifier);
} else {
$sql[] = qsprintf(
$conn,
'(commit.repositoryID = %d
AND commit.commitIdentifier LIKE %>)',
$repo->getID(),
$identifier);
}
}
}
}
if (!$sql) {
// If we discarded all possible identifiers (e.g., they all referenced
// bogus repositories or were all too short), make sure the query finds
// nothing.
throw new PhabricatorEmptyQueryException(
pht('No commit identifiers.'));
}
$where[] = qsprintf($conn, '%LO', $sql);
}
if ($this->auditIDs !== null) {
$where[] = qsprintf(
$conn,
'auditor.id IN (%Ld)',
$this->auditIDs);
}
if ($this->auditorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'auditor.auditorPHID IN (%Ls)',
$this->auditorPHIDs);
}
if ($this->statuses !== null) {
$statuses = DiffusionCommitAuditStatus::newModernKeys(
$this->statuses);
$where[] = qsprintf(
$conn,
'commit.auditStatus IN (%Ls)',
$statuses);
}
if ($this->packagePHIDs !== null) {
$where[] = qsprintf(
$conn,
'package.dst IN (%Ls)',
$this->packagePHIDs);
}
if ($this->unreachable !== null) {
if ($this->unreachable) {
$where[] = qsprintf(
$conn,
'(commit.importStatus & %d) = %d',
PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE,
PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE);
} else {
$where[] = qsprintf(
$conn,
'(commit.importStatus & %d) = 0',
PhabricatorRepositoryCommit::IMPORTED_UNREACHABLE);
}
}
return $where;
}
protected function didFilterResults(array $filtered) {
if ($this->identifierMap) {
foreach ($this->identifierMap as $name => $commit) {
if (isset($filtered[$commit->getPHID()])) {
unset($this->identifierMap[$name]);
}
}
}
}
private function shouldJoinAuditor() {
return ($this->auditIDs || $this->auditorPHIDs);
}
private function shouldJoinOwners() {
return (bool)$this->packagePHIDs;
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$join = parent::buildJoinClauseParts($conn);
$audit_request = new PhabricatorRepositoryAuditRequest();
if ($this->shouldJoinAuditor()) {
$join[] = qsprintf(
$conn,
'JOIN %T auditor ON commit.phid = auditor.commitPHID',
$audit_request->getTableName());
}
if ($this->shouldJoinOwners()) {
$join[] = qsprintf(
$conn,
'JOIN %T package ON commit.phid = package.src
AND package.type = %s',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
DiffusionCommitHasPackageEdgeType::EDGECONST);
}
return $join;
}
protected function shouldGroupQueryResultRows() {
if ($this->shouldJoinAuditor()) {
return true;
}
if ($this->shouldJoinOwners()) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
public function getQueryApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'epoch' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'epoch',
'type' => 'int',
'reverse' => false,
),
);
}
- protected function getPagingValueMap($cursor, array $keys) {
- $commit = $this->loadCursorObject($cursor);
+ protected function newPagingMapFromPartialObject($object) {
return array(
- 'id' => $commit->getID(),
- 'epoch' => $commit->getEpoch(),
+ 'id' => (int)$object->getID(),
+ 'epoch' => (int)$object->getEpoch(),
);
}
public function getBuiltinOrders() {
$parent = parent::getBuiltinOrders();
// Rename the default ID-based orders.
$parent['importnew'] = array(
'name' => pht('Import Date (Newest First)'),
) + $parent['newest'];
$parent['importold'] = array(
'name' => pht('Import Date (Oldest First)'),
) + $parent['oldest'];
return array(
'newest' => array(
'vector' => array('epoch', 'id'),
'name' => pht('Commit Date (Newest First)'),
),
'oldest' => array(
'vector' => array('-epoch', '-id'),
'name' => pht('Commit Date (Oldest First)'),
),
) + $parent;
}
+ private function selectPossibleAuthors(array $phids) {
+ // See PHI1057. Select PHIDs which might possibly be commit authors from
+ // a larger list of PHIDs. This primarily filters out packages and projects
+ // from "Responsible Users: ..." queries. Our goal in performing this
+ // filtering is to improve the performance of the final query.
+
+ foreach ($phids as $key => $phid) {
+ if (phid_get_type($phid) !== PhabricatorPeopleUserPHIDType::TYPECONST) {
+ unset($phids[$key]);
+ }
+ }
+
+ return $phids;
+ }
+
}
diff --git a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php
index a31761461..b760361c1 100644
--- a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php
+++ b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php
@@ -1,312 +1,312 @@
<?php
abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
private $args;
private $repository;
private $hasWriteAccess;
private $shouldProxy;
private $baseRequestPath;
public function getRepository() {
if (!$this->repository) {
throw new Exception(pht('Repository is not available yet!'));
}
return $this->repository;
}
private function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function getArgs() {
return $this->args;
}
public function getEnvironment() {
$env = array(
DiffusionCommitHookEngine::ENV_USER => $this->getSSHUser()->getUsername(),
DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'ssh',
);
$identifier = $this->getRequestIdentifier();
if ($identifier !== null) {
$env[DiffusionCommitHookEngine::ENV_REQUEST] = $identifier;
}
$remote_address = $this->getSSHRemoteAddress();
if ($remote_address !== null) {
$env[DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS] = $remote_address;
}
return $env;
}
/**
* Identify and load the affected repository.
*/
abstract protected function identifyRepository();
abstract protected function executeRepositoryOperations();
abstract protected function raiseWrongVCSException(
PhabricatorRepository $repository);
protected function getBaseRequestPath() {
return $this->baseRequestPath;
}
protected function writeError($message) {
$this->getErrorChannel()->write($message);
return $this;
}
protected function getCurrentDeviceName() {
$device = AlmanacKeys::getLiveDevice();
if ($device) {
return $device->getName();
}
return php_uname('n');
}
protected function shouldProxy() {
return $this->shouldProxy;
}
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) {
throw new Exception(
pht(
'Unable to determine the username to connect with when trying '.
'to proxy an SSH request within the Phabricator cluster.'));
}
$port = $uri->getPort();
$host = $uri->getDomain();
$key_path = AlmanacKeys::getKeyPath('device.key');
if (!Filesystem::pathExists($key_path)) {
throw new Exception(
pht(
'Unable to proxy this SSH request within the cluster: this device '.
'is not registered and has a missing device key (expected to '.
'find key at "%s").',
$key_path));
}
$options = array();
$options[] = '-o';
$options[] = 'StrictHostKeyChecking=no';
$options[] = '-o';
$options[] = 'UserKnownHostsFile=/dev/null';
// This is suppressing "added <address> to the list of known hosts"
// messages, which are confusing and irrelevant when they arise from
// proxied requests. It might also be suppressing lots of useful errors,
- // of course. Ideally, we would enforce host keys eventually.
+ // of course. Ideally, we would enforce host keys eventually. See T13121.
$options[] = '-o';
- $options[] = 'LogLevel=quiet';
+ $options[] = 'LogLevel=ERROR';
// NOTE: We prefix the command with "@username", which the far end of the
// connection will parse in order to act as the specified user. This
// behavior is only available to cluster requests signed by a trusted
// device key.
return csprintf(
'ssh %Ls -l %s -i %s -p %s %s -- %s %Ls',
$options,
$username,
$key_path,
$port,
$host,
'@'.$this->getSSHUser()->getUsername(),
$this->getOriginalArguments());
}
final public function execute(PhutilArgumentParser $args) {
$this->args = $args;
$viewer = $this->getSSHUser();
$have_diffusion = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorDiffusionApplication',
$viewer);
if (!$have_diffusion) {
throw new Exception(
pht(
'You do not have permission to access the Diffusion application, '.
'so you can not interact with repositories over SSH.'));
}
$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,
array(
'neverProxy' => $is_cluster_request,
'protocols' => array(
'ssh',
),
));
$this->shouldProxy = (bool)$uri;
try {
return $this->executeRepositoryOperations();
} catch (Exception $ex) {
$this->writeError(get_class($ex).': '.$ex->getMessage());
return 1;
}
}
protected function loadRepositoryWithPath($path, $vcs) {
$viewer = $this->getSSHUser();
$info = PhabricatorRepository::parseRepositoryServicePath($path, $vcs);
if ($info === null) {
throw new Exception(
pht(
'Unrecognized repository path "%s". Expected a path like "%s", '.
'"%s", or "%s".',
$path,
'/diffusion/X/',
'/diffusion/123/',
'/source/thaumaturgy.git'));
}
$identifier = $info['identifier'];
$base = $info['base'];
$this->baseRequestPath = $base;
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withIdentifiers(array($identifier))
->needURIs(true)
->executeOne();
if (!$repository) {
throw new Exception(
pht('No repository "%s" exists!', $identifier));
}
$is_cluster = $this->getIsClusterRequest();
$protocol = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH;
if (!$repository->canServeProtocol($protocol, false, $is_cluster)) {
throw new Exception(
pht(
'This repository ("%s") is not available over SSH.',
$repository->getDisplayName()));
}
if ($repository->getVersionControlSystem() != $vcs) {
$this->raiseWrongVCSException($repository);
}
return $repository;
}
protected function requireWriteAccess($protocol_command = null) {
if ($this->hasWriteAccess === true) {
return;
}
$repository = $this->getRepository();
$viewer = $this->getSSHUser();
// c4science custo
//if ($viewer->isOmnipotent()) {
// throw new Exception(
// pht(
// 'This request is authenticated as a cluster device, but is '.
// 'performing a write. Writes must be performed with a real '.
// 'user account.'));
//}
$protocol = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH;
if ($repository->canServeProtocol($protocol, true)) {
$can_push = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
DiffusionPushCapability::CAPABILITY);
if (!$can_push) {
throw new Exception(
pht('You do not have permission to push to this repository.'));
}
} else {
if ($protocol_command !== null) {
throw new Exception(
pht(
'This repository is read-only over SSH (tried to execute '.
'protocol command "%s").',
$protocol_command));
} else {
throw new Exception(
pht('This repository is read-only over SSH.'));
}
}
$this->hasWriteAccess = true;
return $this->hasWriteAccess;
}
protected function shouldSkipReadSynchronization() {
$viewer = $this->getSSHUser();
// Currently, the only case where devices interact over SSH without
// assuming user credentials is when synchronizing before a read. These
// synchronizing reads do not themselves need to be synchronized.
if ($viewer->isOmnipotent()) {
return true;
}
return false;
}
protected function newPullEvent() {
$viewer = $this->getSSHUser();
$repository = $this->getRepository();
$remote_address = $this->getSSHRemoteAddress();
return id(new PhabricatorRepositoryPullEvent())
->setEpoch(PhabricatorTime::getNow())
->setRemoteAddress($remote_address)
->setRemoteProtocol(PhabricatorRepositoryPullEvent::PROTOCOL_SSH)
->setPullerPHID($viewer->getPHID())
->setRepositoryPHID($repository->getPHID());
}
}
diff --git a/src/applications/diffusion/view/DiffusionView.php b/src/applications/diffusion/view/DiffusionView.php
index eb7f3eb72..123d22af5 100644
--- a/src/applications/diffusion/view/DiffusionView.php
+++ b/src/applications/diffusion/view/DiffusionView.php
@@ -1,265 +1,265 @@
<?php
abstract class DiffusionView extends AphrontView {
private $diffusionRequest;
final public function setDiffusionRequest(DiffusionRequest $request) {
$this->diffusionRequest = $request;
return $this;
}
final public function getDiffusionRequest() {
return $this->diffusionRequest;
}
final public function linkHistory($path) {
$href = $this->getDiffusionRequest()->generateURI(
array(
'action' => 'history',
'path' => $path,
));
return $this->renderHistoryLink($href);
}
final public function linkBranchHistory($branch) {
$href = $this->getDiffusionRequest()->generateURI(
array(
'action' => 'history',
'branch' => $branch,
));
return $this->renderHistoryLink($href);
}
final public function linkTagHistory($tag) {
$href = $this->getDiffusionRequest()->generateURI(
array(
'action' => 'history',
'commit' => $tag,
));
return $this->renderHistoryLink($href);
}
private function renderHistoryLink($href) {
return javelin_tag(
'a',
array(
'href' => $href,
'class' => 'diffusion-link-icon',
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => pht('History'),
'align' => 'E',
),
),
id(new PHUIIconView())->setIcon('fa-history bluegrey'));
}
final public function linkBrowse(
$path,
array $details = array(),
$button = false) {
require_celerity_resource('diffusion-icons-css');
Javelin::initBehavior('phabricator-tooltips');
$file_type = idx($details, 'type');
unset($details['type']);
$display_name = idx($details, 'name');
unset($details['name']);
if (strlen($display_name)) {
$display_name = phutil_tag(
'span',
array(
'class' => 'diffusion-browse-name',
),
$display_name);
}
if (isset($details['external'])) {
- $href = id(new PhutilURI('/diffusion/external/'))
- ->setQueryParams(
- array(
- 'uri' => idx($details, 'external'),
- 'id' => idx($details, 'hash'),
- ));
+ $params = array(
+ 'uri' => idx($details, 'external'),
+ 'id' => idx($details, 'hash'),
+ );
+
+ $href = new PhutilURI('/diffusion/external/', $params);
$tip = pht('Browse External');
} else {
$href = $this->getDiffusionRequest()->generateURI(
$details + array(
'action' => 'browse',
'path' => $path,
));
$tip = pht('Browse');
}
$icon = DifferentialChangeType::getIconForFileType($file_type);
$color = DifferentialChangeType::getIconColorForFileType($file_type);
$icon_view = id(new PHUIIconView())
->setIcon($icon.' '.$color);
// If we're rendering a file or directory name, don't show the tooltip.
if ($display_name !== null) {
$sigil = null;
$meta = null;
} else {
$sigil = 'has-tooltip';
$meta = array(
'tip' => $tip,
'align' => 'E',
);
}
if ($button) {
return id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-code')
->setHref($href)
->setToolTip(pht('Browse'))
->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE);
}
return javelin_tag(
'a',
array(
'href' => $href,
'class' => 'diffusion-link-icon',
'sigil' => $sigil,
'meta' => $meta,
),
array(
$icon_view,
$display_name,
));
}
final public static function linkCommit(
PhabricatorRepository $repository,
$commit,
$summary = '') {
$commit_name = $repository->formatCommitName($commit, $local = true);
if (strlen($summary)) {
$commit_name .= ': '.$summary;
}
return phutil_tag(
'a',
array(
'href' => $repository->getCommitURI($commit),
),
$commit_name);
}
final public static function linkDetail(
PhabricatorRepository $repository,
$commit,
$detail) {
return phutil_tag(
'a',
array(
'href' => $repository->getCommitURI($commit),
),
$detail);
}
final public static function linkRevision($id) {
if (!$id) {
return null;
}
return phutil_tag(
'a',
array(
'href' => "/D{$id}",
),
"D{$id}");
}
final public static function renderName($name) {
$email = new PhutilEmailAddress($name);
if ($email->getDisplayName() && $email->getDomainName()) {
Javelin::initBehavior('phabricator-tooltips', array());
require_celerity_resource('aphront-tooltip-css');
return javelin_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $email->getAddress(),
'align' => 'S',
'size' => 'auto',
),
),
$email->getDisplayName());
}
return hsprintf('%s', $name);
}
final protected function renderBuildable(
HarbormasterBuildable $buildable,
$type = null) {
Javelin::initBehavior('phabricator-tooltips');
$icon = $buildable->getStatusIcon();
$color = $buildable->getStatusColor();
$name = $buildable->getStatusDisplayName();
if ($type == 'button') {
return id(new PHUIButtonView())
->setTag('a')
->setText($name)
->setIcon($icon)
->setColor($color)
->setHref('/'.$buildable->getMonogram())
->addClass('mmr')
->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE)
->addClass('diffusion-list-build-status');
}
return id(new PHUIIconView())
->setIcon($icon.' '.$color)
->addSigil('has-tooltip')
->setHref('/'.$buildable->getMonogram())
->setMetadata(
array(
'tip' => $name,
));
}
final protected function loadBuildables(array $commits) {
assert_instances_of($commits, 'PhabricatorRepositoryCommit');
if (!$commits) {
return array();
}
$viewer = $this->getUser();
$harbormaster_app = 'PhabricatorHarbormasterApplication';
$have_harbormaster = PhabricatorApplication::isClassInstalledForViewer(
$harbormaster_app,
$viewer);
if ($have_harbormaster) {
$buildables = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withBuildablePHIDs(mpull($commits, 'getPHID'))
->withManualBuildables(false)
->execute();
$buildables = mpull($buildables, null, 'getBuildablePHID');
} else {
$buildables = array();
}
return $buildables;
}
}
diff --git a/src/applications/diviner/markup/DivinerSymbolRemarkupRule.php b/src/applications/diviner/markup/DivinerSymbolRemarkupRule.php
index 7fdb4cb37..db6892762 100644
--- a/src/applications/diviner/markup/DivinerSymbolRemarkupRule.php
+++ b/src/applications/diviner/markup/DivinerSymbolRemarkupRule.php
@@ -1,167 +1,167 @@
<?php
final class DivinerSymbolRemarkupRule extends PhutilRemarkupRule {
const KEY_RULE_ATOM_REF = 'rule.diviner.atomref';
public function getPriority() {
return 200.0;
}
public function apply($text) {
// Grammar here is:
//
// rule = '@{' maybe_type name maybe_title '}'
// maybe_type = null | type ':' | type '@' book ':'
// name = name | name '@' context
// maybe_title = null | '|' title
//
// So these are all valid:
//
// @{name}
// @{type : name}
// @{name | title}
// @{type @ book : name @ context | title}
return preg_replace_callback(
'/(?:^|\B)@{'.
'(?:(?P<type>[^:]+?):)?'.
'(?P<name>[^}|]+?)'.
'(?:[|](?P<title>[^}]+))?'.
'}/',
array($this, 'markupSymbol'),
$text);
}
public function markupSymbol(array $matches) {
if (!$this->isFlatText($matches[0])) {
return $matches[0];
}
$type = (string)idx($matches, 'type');
$name = (string)$matches['name'];
$title = (string)idx($matches, 'title');
// Collapse sequences of whitespace into a single space.
$type = preg_replace('/\s+/', ' ', trim($type));
$name = preg_replace('/\s+/', ' ', trim($name));
$title = preg_replace('/\s+/', ' ', trim($title));
$ref = array();
if (strpos($type, '@') !== false) {
list($type, $book) = explode('@', $type, 2);
$ref['type'] = trim($type);
$ref['book'] = trim($book);
} else {
$ref['type'] = $type;
}
if (strpos($name, '@') !== false) {
list($name, $context) = explode('@', $name, 2);
$ref['name'] = trim($name);
$ref['context'] = trim($context);
} else {
$ref['name'] = $name;
}
$ref['title'] = nonempty($title, $name);
foreach ($ref as $key => $value) {
if ($value === '') {
unset($ref[$key]);
}
}
$engine = $this->getEngine();
$token = $engine->storeText('');
$key = self::KEY_RULE_ATOM_REF;
$data = $engine->getTextMetadata($key, array());
$data[$token] = $ref;
$engine->setTextMetadata($key, $data);
return $token;
}
public function didMarkupText() {
$engine = $this->getEngine();
$key = self::KEY_RULE_ATOM_REF;
$data = $engine->getTextMetadata($key, array());
$renderer = $engine->getConfig('diviner.renderer');
foreach ($data as $token => $ref_dict) {
$ref = DivinerAtomRef::newFromDictionary($ref_dict);
$title = $ref->getTitle();
$href = null;
if ($renderer) {
// Here, we're generating documentation. If possible, we want to find
// the real atom ref so we can render the correct default title and
// render invalid links in an alternate style.
$ref = $renderer->normalizeAtomRef($ref);
if ($ref) {
$title = nonempty($ref->getTitle(), $ref->getName());
$href = $renderer->getHrefForAtomRef($ref);
}
} else {
// Here, we're generating comment text or something like that. Just
// link to Diviner and let it sort things out.
- $href = id(new PhutilURI('/diviner/find/'))
- ->setQueryParams(
- array(
- 'book' => $ref->getBook(),
- 'name' => $ref->getName(),
- 'type' => $ref->getType(),
- 'context' => $ref->getContext(),
- 'jump' => true,
- ));
+ $params = array(
+ 'book' => $ref->getBook(),
+ 'name' => $ref->getName(),
+ 'type' => $ref->getType(),
+ 'context' => $ref->getContext(),
+ 'jump' => true,
+ );
+
+ $href = new PhutilURI('/diviner/find/', $params);
}
// TODO: This probably is not the best place to do this. Move it somewhere
// better when it becomes more clear where it should actually go.
if ($ref) {
switch ($ref->getType()) {
case 'function':
case 'method':
$title = $title.'()';
break;
}
}
if ($this->getEngine()->isTextMode()) {
if ($href) {
$link = $title.' <'.PhabricatorEnv::getProductionURI($href).'>';
} else {
$link = $title;
}
} else if ($href) {
if ($this->getEngine()->isHTMLMailMode()) {
$href = PhabricatorEnv::getProductionURI($href);
}
$link = $this->newTag(
'a',
array(
'class' => 'atom-ref',
'href' => $href,
),
$title);
} else {
$link = $this->newTag(
'span',
array(
'class' => 'atom-ref-invalid',
),
$title);
}
$engine->overwriteStoredText($token, $link);
}
}
}
diff --git a/src/applications/diviner/query/DivinerBookQuery.php b/src/applications/diviner/query/DivinerBookQuery.php
index d540d971b..2d6527ec9 100644
--- a/src/applications/diviner/query/DivinerBookQuery.php
+++ b/src/applications/diviner/query/DivinerBookQuery.php
@@ -1,201 +1,200 @@
<?php
final class DivinerBookQuery extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $names;
private $nameLike;
private $namePrefix;
private $repositoryPHIDs;
private $needProjectPHIDs;
private $needRepositories;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withNameLike($name) {
$this->nameLike = $name;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withNamePrefix($prefix) {
$this->namePrefix = $prefix;
return $this;
}
public function withRepositoryPHIDs(array $repository_phids) {
$this->repositoryPHIDs = $repository_phids;
return $this;
}
public function needProjectPHIDs($need_phids) {
$this->needProjectPHIDs = $need_phids;
return $this;
}
public function needRepositories($need_repositories) {
$this->needRepositories = $need_repositories;
return $this;
}
protected function loadPage() {
$table = new DivinerLiveBook();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($data);
}
protected function didFilterPage(array $books) {
assert_instances_of($books, 'DivinerLiveBook');
if ($this->needRepositories) {
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withPHIDs(mpull($books, 'getRepositoryPHID'))
->execute();
$repositories = mpull($repositories, null, 'getPHID');
foreach ($books as $key => $book) {
if ($book->getRepositoryPHID() === null) {
$book->attachRepository(null);
continue;
}
$repository = idx($repositories, $book->getRepositoryPHID());
if (!$repository) {
$this->didRejectResult($book);
unset($books[$key]);
continue;
}
$book->attachRepository($repository);
}
}
if ($this->needProjectPHIDs) {
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(mpull($books, 'getPHID'))
->withEdgeTypes(
array(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
));
$edge_query->execute();
foreach ($books as $book) {
$project_phids = $edge_query->getDestinationPHIDs(
array(
$book->getPHID(),
));
$book->attachProjectPHIDs($project_phids);
}
}
return $books;
}
protected function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
if ($this->ids) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if (strlen($this->nameLike)) {
$where[] = qsprintf(
$conn,
'name LIKE %~',
$this->nameLike);
}
if ($this->names !== null) {
$where[] = qsprintf(
$conn,
'name IN (%Ls)',
$this->names);
}
if (strlen($this->namePrefix)) {
$where[] = qsprintf(
$conn,
'name LIKE %>',
$this->namePrefix);
}
if ($this->repositoryPHIDs !== null) {
$where[] = qsprintf(
$conn,
'repositoryPHID IN (%Ls)',
$this->repositoryPHIDs);
}
$where[] = $this->buildPagingClause($conn);
return $this->formatWhereClause($conn, $where);
}
public function getQueryApplicationClass() {
return 'PhabricatorDivinerApplication';
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'column' => 'name',
'type' => 'string',
'reverse' => true,
'unique' => true,
),
);
}
- protected function getPagingValueMap($cursor, array $keys) {
- $book = $this->loadCursorObject($cursor);
-
+ protected function newPagingMapFromPartialObject($object) {
return array(
- 'name' => $book->getName(),
+ 'id' => (int)$object->getID(),
+ 'name' => $object->getName(),
);
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Name'),
),
) + parent::getBuiltinOrders();
}
}
diff --git a/src/applications/diviner/storage/DivinerLiveBookTransaction.php b/src/applications/diviner/storage/DivinerLiveBookTransaction.php
index ae461e751..f8eb81d1f 100644
--- a/src/applications/diviner/storage/DivinerLiveBookTransaction.php
+++ b/src/applications/diviner/storage/DivinerLiveBookTransaction.php
@@ -1,18 +1,14 @@
<?php
final class DivinerLiveBookTransaction
extends PhabricatorApplicationTransaction {
public function getApplicationName() {
return 'diviner';
}
public function getApplicationTransactionType() {
return DivinerBookPHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return null;
- }
-
}
diff --git a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php
index 1aab14b57..b1eebd92a 100644
--- a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php
+++ b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php
@@ -1,59 +1,62 @@
<?php
final class DrydockSSHCommandInterface extends DrydockCommandInterface {
private $credential;
private $connectTimeout;
private function loadCredential() {
if ($this->credential === null) {
$credential_phid = $this->getConfig('credentialPHID');
$this->credential = PassphraseSSHKey::loadFromPHID(
$credential_phid,
PhabricatorUser::getOmnipotentUser());
}
return $this->credential;
}
public function setConnectTimeout($timeout) {
$this->connectTimeout = $timeout;
return $this;
}
public function getExecFuture($command) {
$credential = $this->loadCredential();
$argv = func_get_args();
$argv = $this->applyWorkingDirectoryToArgv($argv);
$full_command = call_user_func_array('csprintf', $argv);
$flags = array();
+
+ // See T13121. Attempt to suppress the "Permanently added X to list of
+ // known hosts" message without suppressing anything important.
$flags[] = '-o';
- $flags[] = 'LogLevel=quiet';
+ $flags[] = 'LogLevel=ERROR';
$flags[] = '-o';
$flags[] = 'StrictHostKeyChecking=no';
$flags[] = '-o';
$flags[] = 'UserKnownHostsFile=/dev/null';
$flags[] = '-o';
$flags[] = 'BatchMode=yes';
if ($this->connectTimeout) {
$flags[] = '-o';
$flags[] = 'ConnectTimeout='.$this->connectTimeout;
}
return new ExecFuture(
'ssh %Ls -l %P -p %s -i %P %s -- %s',
$flags,
$credential->getUsernameEnvelope(),
$this->getConfig('port'),
$credential->getKeyfileEnvelope(),
$this->getConfig('host'),
$full_command);
}
}
diff --git a/src/applications/drydock/operation/DrydockLandRepositoryOperation.php b/src/applications/drydock/operation/DrydockLandRepositoryOperation.php
index 1ccc82eb7..acb48f6f0 100644
--- a/src/applications/drydock/operation/DrydockLandRepositoryOperation.php
+++ b/src/applications/drydock/operation/DrydockLandRepositoryOperation.php
@@ -1,442 +1,442 @@
<?php
final class DrydockLandRepositoryOperation
extends DrydockRepositoryOperationType {
const OPCONST = 'land';
const PHASE_PUSH = 'op.land.push';
const PHASE_COMMIT = 'op.land.commit';
public function getOperationDescription(
DrydockRepositoryOperation $operation,
PhabricatorUser $viewer) {
return pht('Land Revision');
}
public function getOperationCurrentStatus(
DrydockRepositoryOperation $operation,
PhabricatorUser $viewer) {
$target = $operation->getRepositoryTarget();
$repository = $operation->getRepository();
switch ($operation->getOperationState()) {
case DrydockRepositoryOperation::STATE_WAIT:
return pht(
'Waiting to land revision into %s on %s...',
$repository->getMonogram(),
$target);
case DrydockRepositoryOperation::STATE_WORK:
return pht(
'Landing revision into %s on %s...',
$repository->getMonogram(),
$target);
case DrydockRepositoryOperation::STATE_DONE:
return pht(
'Revision landed into %s.',
$repository->getMonogram());
}
}
public function getWorkingCopyMerges(DrydockRepositoryOperation $operation) {
$repository = $operation->getRepository();
$merges = array();
$object = $operation->getObject();
if ($object instanceof DifferentialRevision) {
$diff = $this->loadDiff($operation);
$merges[] = array(
'src.uri' => $repository->getStagingURI(),
'src.ref' => $diff->getStagingRef(),
);
} else {
throw new Exception(
pht(
'Invalid or unknown object ("%s") for land operation, expected '.
'Differential Revision.',
$operation->getObjectPHID()));
}
return $merges;
}
public function applyOperation(
DrydockRepositoryOperation $operation,
DrydockInterface $interface) {
$viewer = $this->getViewer();
$repository = $operation->getRepository();
$cmd = array();
$arg = array();
$object = $operation->getObject();
if ($object instanceof DifferentialRevision) {
$revision = $object;
$diff = $this->loadDiff($operation);
$dict = $diff->getDiffAuthorshipDict();
$author_name = idx($dict, 'authorName');
$author_email = idx($dict, 'authorEmail');
$api_method = 'differential.getcommitmessage';
$api_params = array(
'revision_id' => $revision->getID(),
);
$commit_message = id(new ConduitCall($api_method, $api_params))
->setUser($viewer)
->execute();
} else {
throw new Exception(
pht(
'Invalid or unknown object ("%s") for land operation, expected '.
'Differential Revision.',
$operation->getObjectPHID()));
}
$target = $operation->getRepositoryTarget();
list($type, $name) = explode(':', $target, 2);
switch ($type) {
case 'branch':
$push_dst = 'refs/heads/'.$name;
break;
default:
throw new Exception(
pht(
'Unknown repository operation target type "%s" (in target "%s").',
$type,
$target));
}
$committer_info = $this->getCommitterInfo($operation);
// NOTE: We're doing this commit with "-F -" so we don't run into trouble
// with enormous commit messages which might otherwise exceed the maximum
// size of a command.
$future = $interface->getExecFuture(
'git -c user.name=%s -c user.email=%s commit --author %s -F - --',
$committer_info['name'],
$committer_info['email'],
"{$author_name} <{$author_email}>");
$future->write($commit_message);
try {
$future->resolvex();
} catch (CommandException $ex) {
$display_command = csprintf('git commit');
// TODO: One reason this can fail is if the changes have already been
// merged. We could try to detect that.
$error = DrydockCommandError::newFromCommandException($ex)
->setPhase(self::PHASE_COMMIT)
->setDisplayCommand($display_command);
$operation->setCommandError($error->toDictionary());
throw $ex;
}
try {
$interface->execx(
'git push origin -- %s:%s',
'HEAD',
$push_dst);
} catch (CommandException $ex) {
$display_command = csprintf(
'git push origin %R:%R',
'HEAD',
$push_dst);
$error = DrydockCommandError::newFromCommandException($ex)
->setPhase(self::PHASE_PUSH)
->setDisplayCommand($display_command);
$operation->setCommandError($error->toDictionary());
throw $ex;
}
}
private function getCommitterInfo(DrydockRepositoryOperation $operation) {
$viewer = $this->getViewer();
$committer_name = null;
$author_phid = $operation->getAuthorPHID();
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($author_phid))
->executeOne();
if ($object) {
if ($object instanceof PhabricatorUser) {
$committer_name = $object->getUsername();
}
}
if (!strlen($committer_name)) {
$committer_name = pht('autocommitter');
}
// TODO: Probably let users choose a VCS email address in settings. For
// now just make something up so we don't leak anyone's stuff.
return array(
'name' => $committer_name,
'email' => 'autocommitter@example.com',
);
}
private function loadDiff(DrydockRepositoryOperation $operation) {
$viewer = $this->getViewer();
$revision = $operation->getObject();
$diff_phid = $operation->getProperty('differential.diffPHID');
$diff = id(new DifferentialDiffQuery())
->setViewer($viewer)
->withPHIDs(array($diff_phid))
->executeOne();
if (!$diff) {
throw new Exception(
pht(
'Unable to load diff "%s".',
$diff_phid));
}
$diff_revid = $diff->getRevisionID();
$revision_id = $revision->getID();
if ($diff_revid != $revision_id) {
throw new Exception(
pht(
'Diff ("%s") has wrong revision ID ("%s", expected "%s").',
$diff_phid,
$diff_revid,
$revision_id));
}
return $diff;
}
public function getBarrierToLanding(
PhabricatorUser $viewer,
DifferentialRevision $revision) {
$repository = $revision->getRepository();
if (!$repository) {
return array(
'title' => pht('No Repository'),
'body' => pht(
'This revision is not associated with a known repository. Only '.
'revisions associated with a tracked repository can be landed '.
'automatically.'),
);
}
if (!$repository->canPerformAutomation()) {
return array(
'title' => pht('No Repository Automation'),
'body' => pht(
'The repository this revision is associated with ("%s") is not '.
'configured to support automation. Configure automation for the '.
'repository to enable revisions to be landed automatically.',
$repository->getMonogram()),
);
}
// Check if this diff was pushed to a staging area.
$diff = id(new DifferentialDiffQuery())
->setViewer($viewer)
->withIDs(array($revision->getActiveDiff()->getID()))
->needProperties(true)
->executeOne();
// Older diffs won't have this property. They may still have been pushed.
// At least for now, assume staging changes are present if the property
// is missing. This should smooth the transition to the more formal
// approach.
$has_staging = $diff->hasDiffProperty('arc.staging');
if ($has_staging) {
$staging = $diff->getProperty('arc.staging');
if (!is_array($staging)) {
$staging = array();
}
$status = idx($staging, 'status');
if ($status != ArcanistDiffWorkflow::STAGING_PUSHED) {
return $this->getBarrierToLandingFromStagingStatus($status);
}
}
// TODO: At some point we should allow installs to give "land reviewed
// code" permission to more users than "push any commit", because it is
// a much less powerful operation. For now, just require push so this
// doesn't do anything users can't do on their own.
$can_push = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
DiffusionPushCapability::CAPABILITY);
if (!$can_push) {
return array(
'title' => pht('Unable to Push'),
'body' => pht(
'You do not have permission to push to the repository this '.
'revision is associated with ("%s"), so you can not land it.',
$repository->getMonogram()),
);
}
if ($revision->isAccepted()) {
// We can land accepted revisions, so continue below. Otherwise, raise
// an error with tailored messaging for the most common cases.
} else if ($revision->isAbandoned()) {
return array(
'title' => pht('Revision Abandoned'),
'body' => pht(
'This revision has been abandoned. Only accepted revisions '.
'may land.'),
);
} else if ($revision->isClosed()) {
return array(
'title' => pht('Revision Closed'),
'body' => pht(
'This revision has already been closed. Only open, accepted '.
'revisions may land.'),
);
} else {
return array(
'title' => pht('Revision Not Accepted'),
'body' => pht(
'This revision is still under review. Only revisions which '.
'have been accepted may land.'),
);
}
// Check for other operations. Eventually this should probably be more
// general (e.g., it's OK to land to multiple different branches
// simultaneously) but just put this in as a sanity check for now.
$other_operations = id(new DrydockRepositoryOperationQuery())
->setViewer($viewer)
->withObjectPHIDs(array($revision->getPHID()))
->withOperationTypes(
array(
$this->getOperationConstant(),
))
->withOperationStates(
array(
DrydockRepositoryOperation::STATE_WAIT,
DrydockRepositoryOperation::STATE_WORK,
DrydockRepositoryOperation::STATE_DONE,
))
->execute();
if ($other_operations) {
$any_done = false;
foreach ($other_operations as $operation) {
if ($operation->isDone()) {
$any_done = true;
break;
}
}
if ($any_done) {
return array(
'title' => pht('Already Complete'),
'body' => pht('This revision has already landed.'),
);
} else {
return array(
'title' => pht('Already In Flight'),
'body' => pht('This revision is already landing.'),
);
}
}
return null;
}
private function getBarrierToLandingFromStagingStatus($status) {
switch ($status) {
case ArcanistDiffWorkflow::STAGING_USER_SKIP:
return array(
'title' => pht('Staging Area Skipped'),
'body' => pht(
'The diff author used the %s flag to skip pushing this change to '.
'staging. Changes must be pushed to staging before they can be '.
'landed from the web.',
phutil_tag('tt', array(), '--skip-staging')),
);
case ArcanistDiffWorkflow::STAGING_DIFF_RAW:
return array(
'title' => pht('Raw Diff Source'),
'body' => pht(
'The diff was generated from a raw input source, so the change '.
'could not be pushed to staging. Changes must be pushed to '.
'staging before they can be landed from the web.'),
);
case ArcanistDiffWorkflow::STAGING_REPOSITORY_UNKNOWN:
return array(
'title' => pht('Unknown Repository'),
'body' => pht(
'When the diff was generated, the client was not able to '.
'determine which repository it belonged to, so the change '.
'was not pushed to staging. Changes must be pushed to staging '.
'before they can be landed from the web.'),
);
case ArcanistDiffWorkflow::STAGING_REPOSITORY_UNAVAILABLE:
return array(
'title' => pht('Staging Unavailable'),
'body' => pht(
'When this diff was generated, the server was running an older '.
'version of Phabricator which did not support staging areas, so '.
'the change was not pushed to staging. Changes must be pushed '.
'to staging before they can be landed from the web.'),
);
case ArcanistDiffWorkflow::STAGING_REPOSITORY_UNSUPPORTED:
return array(
'title' => pht('Repository Unsupported'),
'body' => pht(
'When this diff was generated, the server was running an older '.
'version of Phabricator which did not support staging areas for '.
- 'this version control system, so the chagne was not pushed to '.
+ 'this version control system, so the change was not pushed to '.
'staging. Changes must be pushed to staging before they can be '.
'landed from the web.'),
);
case ArcanistDiffWorkflow::STAGING_REPOSITORY_UNCONFIGURED:
return array(
'title' => pht('Repository Unconfigured'),
'body' => pht(
'When this diff was generated, the repository was not configured '.
'with a staging area, so the change was not pushed to staging. '.
'Changes must be pushed to staging before they can be landed '.
'from the web.'),
);
case ArcanistDiffWorkflow::STAGING_CLIENT_UNSUPPORTED:
return array(
'title' => pht('Client Support Unavailable'),
'body' => pht(
'When this diff was generated, the client did not support '.
'staging areas for this version control system, so the change '.
'was not pushed to staging. Changes must be pushed to staging '.
'before they can be landed from the web. Updating the client '.
'may resolve this issue.'),
);
default:
return array(
'title' => pht('Unknown Error'),
'body' => pht(
'When this diff was generated, it was not pushed to staging for '.
'an unknown reason (the status code was "%s"). Changes must be '.
'pushed to staging before they can be landed from the web. '.
'The server may be running an out-of-date version of Phabricator, '.
'and updating may provide more information about this error.',
$status),
);
}
}
}
diff --git a/src/applications/fact/controller/PhabricatorFactHomeController.php b/src/applications/fact/controller/PhabricatorFactHomeController.php
index 82f6a0905..56ffe3930 100644
--- a/src/applications/fact/controller/PhabricatorFactHomeController.php
+++ b/src/applications/fact/controller/PhabricatorFactHomeController.php
@@ -1,59 +1,59 @@
<?php
final class PhabricatorFactHomeController extends PhabricatorFactController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
if ($request->isFormPost()) {
$uri = new PhutilURI('/fact/chart/');
- $uri->setQueryParam('y1', $request->getStr('y1'));
+ $uri->replaceQueryParam('y1', $request->getStr('y1'));
return id(new AphrontRedirectResponse())->setURI($uri);
}
$chart_form = $this->buildChartForm();
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Home'));
$title = pht('Facts');
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild(
array(
$chart_form,
));
}
private function buildChartForm() {
$request = $this->getRequest();
$viewer = $request->getUser();
$specs = PhabricatorFact::getAllFacts();
$options = mpull($specs, 'getName', 'getKey');
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Y-Axis'))
->setName('y1')
->setOptions($options))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Plot Chart')));
$panel = new PHUIObjectBoxView();
$panel->setForm($form);
$panel->setHeaderText(pht('Plot Chart'));
return $panel;
}
}
diff --git a/src/applications/feed/config/PhabricatorFeedConfigOptions.php b/src/applications/feed/config/PhabricatorFeedConfigOptions.php
index 4b6612f93..29c5a9549 100644
--- a/src/applications/feed/config/PhabricatorFeedConfigOptions.php
+++ b/src/applications/feed/config/PhabricatorFeedConfigOptions.php
@@ -1,42 +1,42 @@
<?php
final class PhabricatorFeedConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Feed');
}
public function getDescription() {
return pht('Feed options.');
}
public function getIcon() {
return 'fa-newspaper-o';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
+ $hooks_help = $this->deformat(pht(<<<EODOC
+IMPORTANT: Feed hooks are deprecated and have been replaced by Webhooks.
+
+You can configure Webhooks in Herald. This configuration option will be removed
+in a future version of Phabricator.
+
+(This legacy option may be configured with a list of URIs; feed stories will
+send to these URIs.)
+EODOC
+ ));
+
return array(
$this->newOption('feed.http-hooks', 'list<string>', array())
->setLocked(true)
- ->setSummary(pht('POST notifications of feed events.'))
- ->setDescription(
- pht(
- "If you set this to a list of HTTP URIs, when a feed story is ".
- "published a task will be created for each URI that posts the ".
- "story data to the URI. Daemons automagically retry failures 100 ".
- "times, waiting `\$fail_count * 60s` between each subsequent ".
- "failure. Be sure to keep the daemon console (`%s`) open ".
- "while developing and testing your end points. You may need to".
- "restart your daemons to start sending HTTP requests.\n\n".
- "NOTE: URIs are not validated, the URI must return HTTP status ".
- "200 within 30 seconds, and no permission checks are performed.",
- '/daemon/')),
+ ->setSummary(pht('Deprecated.'))
+ ->setDescription($hooks_help),
);
}
}
diff --git a/src/applications/feed/query/PhabricatorFeedQuery.php b/src/applications/feed/query/PhabricatorFeedQuery.php
index c8289d790..5adf65917 100644
--- a/src/applications/feed/query/PhabricatorFeedQuery.php
+++ b/src/applications/feed/query/PhabricatorFeedQuery.php
@@ -1,180 +1,188 @@
<?php
final class PhabricatorFeedQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $filterPHIDs;
private $filterOutPHIDs; // c4science custo
private $chronologicalKeys;
private $rangeMin;
private $rangeMax;
public function withFilterPHIDs(array $phids) {
$this->filterPHIDs = $phids;
return $this;
}
public function withChronologicalKeys(array $keys) {
$this->chronologicalKeys = $keys;
return $this;
}
public function withEpochInRange($range_min, $range_max) {
$this->rangeMin = $range_min;
$this->rangeMax = $range_max;
return $this;
}
public function newResultObject() {
return new PhabricatorFeedStoryData();
}
protected function loadPage() {
// NOTE: We return raw rows from this method, which is a little unusual.
return $this->loadStandardPageRows($this->newResultObject());
}
protected function willFilterPage(array $data) {
$stories = PhabricatorFeedStory::loadAllFromRows($data, $this->getViewer());
foreach ($stories as $key => $story) {
if (!$story->isVisibleInFeed()) {
unset($stories[$key]);
}
}
return $stories;
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
// NOTE: We perform this join unconditionally (even if we have no filter
// PHIDs) to omit rows which have no story references. These story data
// rows are notifications or realtime alerts.
$ref_table = new PhabricatorFeedStoryReference();
$joins[] = qsprintf(
$conn,
'JOIN %T ref ON ref.chronologicalKey = story.chronologicalKey',
$ref_table->getTableName());
return $joins;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->filterPHIDs) { // c4science custo
$where[] = qsprintf(
$conn,
'ref.objectPHID IN (%Ls)',
$this->filterPHIDs);
}
// C4science customization
if ($this->filterOutPHIDs !== null) {
$where[] = qsprintf(
$conn,
'ref.objectPHID NOT IN (%Ls)',
$this->filterOutPHIDs);
}
if ($this->chronologicalKeys !== null) {
// NOTE: We can't use "%d" to format these large integers on 32-bit
// systems. Historically, we formatted these into integers in an
// awkward way because MySQL could sometimes (?) fail to use the proper
// keys if the values were formatted as strings instead of integers.
// After the "qsprintf()" update to use PhutilQueryString, we can no
// longer do this in a sneaky way. However, the MySQL key issue also
// no longer appears to reproduce across several systems. So: just use
// strings until problems turn up?
$where[] = qsprintf(
$conn,
'ref.chronologicalKey IN (%Ls)',
$this->chronologicalKeys);
}
// NOTE: We may not have 64-bit PHP, so do the shifts in MySQL instead.
// From EXPLAIN, it appears like MySQL is smart enough to compute the
// result and make use of keys to execute the query.
if ($this->rangeMin !== null) {
$where[] = qsprintf(
$conn,
'ref.chronologicalKey >= (%d << 32)',
$this->rangeMin);
}
if ($this->rangeMax !== null) {
$where[] = qsprintf(
$conn,
'ref.chronologicalKey < (%d << 32)',
$this->rangeMax);
}
return $where;
}
protected function buildGroupClause(AphrontDatabaseConnection $conn) {
if ($this->filterPHIDs !== null) {
return qsprintf($conn, 'GROUP BY ref.chronologicalKey');
} else {
return qsprintf($conn, 'GROUP BY story.chronologicalKey');
}
}
protected function getDefaultOrderVector() {
return array('key');
}
public function getBuiltinOrders() {
return array(
'newest' => array(
'vector' => array('key'),
'name' => pht('Creation (Newest First)'),
'aliases' => array('created'),
),
'oldest' => array(
'vector' => array('-key'),
'name' => pht('Creation (Oldest First)'),
),
);
}
public function getOrderableColumns() {
$table = ($this->filterPHIDs ? 'ref' : 'story');
return array(
'key' => array(
'table' => $table,
'column' => 'chronologicalKey',
'type' => 'string',
'unique' => true,
),
);
}
- protected function getPagingValueMap($cursor, array $keys) {
+ protected function applyExternalCursorConstraintsToQuery(
+ PhabricatorCursorPagedPolicyAwareQuery $subquery,
+ $cursor) {
+ $subquery->withChronologicalKeys(array($cursor));
+ }
+
+ protected function newExternalCursorStringForResult($object) {
+ return $object->getChronologicalKey();
+ }
+
+ protected function newPagingMapFromPartialObject($object) {
+ // This query is unusual, and the "object" is a raw result row.
return array(
- 'key' => $cursor,
+ 'key' => $object['chronologicalKey'],
);
}
- protected function getResultCursor($item) {
- if ($item instanceof PhabricatorFeedStory) {
- return $item->getChronologicalKey();
- }
- return $item['chronologicalKey'];
+ protected function getPrimaryTableAlias() {
+ return 'story';
}
protected function getPrimaryTableAlias() {
return 'story';
}
public function getQueryApplicationClass() {
return 'PhabricatorFeedApplication';
}
}
diff --git a/src/applications/feed/worker/FeedPublisherHTTPWorker.php b/src/applications/feed/worker/FeedPublisherHTTPWorker.php
index 4742e52a2..27a869dde 100644
--- a/src/applications/feed/worker/FeedPublisherHTTPWorker.php
+++ b/src/applications/feed/worker/FeedPublisherHTTPWorker.php
@@ -1,39 +1,44 @@
<?php
final class FeedPublisherHTTPWorker extends FeedPushWorker {
protected function doWork() {
if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
// Don't invoke hooks in silent mode.
return;
}
$story = $this->loadFeedStory();
$data = $story->getStoryData();
$uri = idx($this->getTaskData(), 'uri');
$valid_uris = PhabricatorEnv::getEnvConfig('feed.http-hooks');
if (!in_array($uri, $valid_uris)) {
throw new PhabricatorWorkerPermanentFailureException();
}
$post_data = array(
'storyID' => $data->getID(),
'storyType' => $data->getStoryType(),
'storyData' => $data->getStoryData(),
'storyAuthorPHID' => $data->getAuthorPHID(),
'storyText' => $story->renderText(),
'epoch' => $data->getEpoch(),
);
+ // NOTE: We're explicitly using "http_build_query()" here because the
+ // "storyData" parameter may be a nested object with arbitrary nested
+ // sub-objects.
+ $post_data = http_build_query($post_data, '', '&');
+
id(new HTTPSFuture($uri, $post_data))
->setMethod('POST')
->setTimeout(30)
->resolvex();
}
public function getWaitBeforeRetry(PhabricatorWorkerTask $task) {
return max($task->getFailureCount(), 1) * 60;
}
}
diff --git a/src/applications/files/controller/PhabricatorFileDataController.php b/src/applications/files/controller/PhabricatorFileDataController.php
index dd5e0adb7..8ceb1e790 100644
--- a/src/applications/files/controller/PhabricatorFileDataController.php
+++ b/src/applications/files/controller/PhabricatorFileDataController.php
@@ -1,227 +1,227 @@
<?php
final class PhabricatorFileDataController extends PhabricatorFileController {
private $phid;
private $key;
private $file;
public function shouldRequireLogin() {
return false;
}
public function shouldAllowPartialSessions() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$this->phid = $request->getURIData('phid');
$this->key = $request->getURIData('key');
$alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain');
$base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
$alt_uri = new PhutilURI($alt);
$alt_domain = $alt_uri->getDomain();
$req_domain = $request->getHost();
$main_domain = id(new PhutilURI($base_uri))->getDomain();
$request_kind = $request->getURIData('kind');
$is_download = ($request_kind === 'download');
if (!strlen($alt) || $main_domain == $alt_domain) {
// No alternate domain.
$should_redirect = false;
$is_alternate_domain = false;
} else if ($req_domain != $alt_domain) {
// Alternate domain, but this request is on the main domain.
$should_redirect = true;
$is_alternate_domain = false;
} else {
// Alternate domain, and on the alternate domain.
$should_redirect = false;
$is_alternate_domain = true;
}
$response = $this->loadFile();
if ($response) {
return $response;
}
$file = $this->getFile();
if ($should_redirect) {
return id(new AphrontRedirectResponse())
->setIsExternal(true)
->setURI($file->getCDNURI($request_kind));
}
$response = new AphrontFileResponse();
$response->setCacheDurationInSeconds(60 * 60 * 24 * 30);
$response->setCanCDN($file->getCanCDN());
$begin = null;
$end = null;
// NOTE: It's important to accept "Range" requests when playing audio.
// If we don't, Safari has difficulty figuring out how long sounds are
// and glitches when trying to loop them. In particular, Safari sends
// an initial request for bytes 0-1 of the audio file, and things go south
// if we can't respond with a 206 Partial Content.
$range = $request->getHTTPHeader('range');
if (strlen($range)) {
list($begin, $end) = $response->parseHTTPRange($range);
}
if (!$file->isViewableInBrowser()) {
$is_download = true;
}
$request_type = $request->getHTTPHeader('X-Phabricator-Request-Type');
$is_lfs = ($request_type == 'git-lfs');
if (!$is_download) {
$response->setMimeType($file->getViewableMimeType());
} else {
$is_post = $request->isHTTPPost();
$is_public = !$viewer->isLoggedIn();
// NOTE: Require POST to download files from the primary domain. If the
// request is not a POST request but arrives on the primary domain, we
// render a confirmation dialog. For discussion, see T13094.
// There are two exceptions to this rule:
// Git LFS requests can download with GET. This is safe (Git LFS won't
// execute files it downloads) and necessary to support Git LFS.
// Requests with no credentials may also download with GET. This
// primarily supports downloading files with `arc download` or other
// API clients. This is only "mostly" safe: if you aren't logged in, you
// are likely immune to XSS and CSRF. However, an attacker may still be
// able to set cookies on this domain (for example, to fixate your
// session). For now, we accept these risks because users running
// Phabricator in this mode are knowingly accepting a security risk
// against setup advice, and there's significant value in having
// API development against test and production installs work the same
// way.
$is_safe = ($is_alternate_domain || $is_post || $is_lfs || $is_public);
if (!$is_safe) {
return $this->newDialog()
->setSubmitURI($file->getDownloadURI())
->setTitle(pht('Download File'))
->appendParagraph(
pht(
'Download file %s (%s)?',
phutil_tag('strong', array(), $file->getName()),
phutil_format_bytes($file->getByteSize())))
->addCancelButton($file->getURI())
->addSubmitButton(pht('Download File'));
}
$response->setMimeType($file->getMimeType());
$response->setDownload($file->getName());
}
$iterator = $file->getFileDataIterator($begin, $end);
$response->setContentLength($file->getByteSize());
$response->setContentIterator($iterator);
// In Chrome, we must permit this domain in "object-src" CSP when serving a
// PDF or the browser will refuse to render it.
if (!$is_download && $file->isPDF()) {
$request_uri = id(clone $request->getAbsoluteRequestURI())
->setPath(null)
->setFragment(null)
- ->setQueryParams(array());
+ ->removeAllQueryParams();
$response->addContentSecurityPolicyURI(
'object-src',
(string)$request_uri);
}
return $response;
}
private function loadFile() {
// Access to files is provided by knowledge of a per-file secret key in
// the URI. Knowledge of this secret is sufficient to retrieve the file.
// For some requests, we also have a valid viewer. However, for many
// requests (like alternate domain requests or Git LFS requests) we will
// not. Even if we do have a valid viewer, use the omnipotent viewer to
// make this logic simpler and more consistent.
// Beyond making the policy check itself more consistent, this also makes
// sure we're consistent about returning HTTP 404 on bad requests instead
// of serving HTTP 200 with a login page, which can mislead some clients.
$viewer = PhabricatorUser::getOmnipotentUser();
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($this->phid))
->withIsDeleted(false)
->executeOne();
if (!$file) {
return new Aphront404Response();
}
// We may be on the CDN domain, so we need to use a fully-qualified URI
// here to make sure we end up back on the main domain.
$info_uri = PhabricatorEnv::getURI($file->getInfoURI());
if (!$file->validateSecretKey($this->key)) {
$dialog = $this->newDialog()
->setTitle(pht('Invalid Authorization'))
->appendParagraph(
pht(
'The link you followed to access this file is no longer '.
'valid. The visibility of the file may have changed after '.
'the link was generated.'))
->appendParagraph(
pht(
'You can continue to the file detail page to get more '.
'information and attempt to access the file.'))
->addCancelButton($info_uri, pht('Continue'));
return id(new AphrontDialogResponse())
->setDialog($dialog)
->setHTTPResponseCode(404);
}
if ($file->getIsPartial()) {
$dialog = $this->newDialog()
->setTitle(pht('Partial Upload'))
->appendParagraph(
pht(
'This file has only been partially uploaded. It must be '.
'uploaded completely before you can download it.'))
->appendParagraph(
pht(
'You can continue to the file detail page to monitor the '.
'upload progress of the file.'))
->addCancelButton($info_uri, pht('Continue'));
return id(new AphrontDialogResponse())
->setDialog($dialog)
->setHTTPResponseCode(404);
}
$this->file = $file;
return null;
}
private function getFile() {
if (!$this->file) {
throw new PhutilInvalidStateException('loadFile');
}
return $this->file;
}
}
diff --git a/src/applications/files/controller/PhabricatorFileLightboxController.php b/src/applications/files/controller/PhabricatorFileLightboxController.php
index 1f679d621..59a826dd4 100644
--- a/src/applications/files/controller/PhabricatorFileLightboxController.php
+++ b/src/applications/files/controller/PhabricatorFileLightboxController.php
@@ -1,109 +1,109 @@
<?php
final class PhabricatorFileLightboxController
extends PhabricatorFileController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$phid = $request->getURIData('phid');
$comment = $request->getStr('comment');
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
if (!$file) {
return new Aphront404Response();
}
if (strlen($comment)) {
$xactions = array();
$xactions[] = id(new PhabricatorFileTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new PhabricatorFileTransactionComment())
->setContent($comment));
$editor = id(new PhabricatorFileEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request);
$editor->applyTransactions($file, $xactions);
}
$transactions = id(new PhabricatorFileTransactionQuery())
->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT));
$timeline = $this->buildTransactionTimeline($file, $transactions);
$comment_form = $this->renderCommentForm($file);
$info = phutil_tag(
'div',
array(
'class' => 'phui-comment-panel-header',
),
$file->getName());
require_celerity_resource('phui-comment-panel-css');
$content = phutil_tag(
'div',
array(
'class' => 'phui-comment-panel',
),
array(
$info,
$timeline,
$comment_form,
));
return id(new AphrontAjaxResponse())
->setContent($content);
}
private function renderCommentForm(PhabricatorFile $file) {
$viewer = $this->getViewer();
if (!$viewer->isLoggedIn()) {
$login_href = id(new PhutilURI('/auth/start/'))
- ->setQueryParam('next', '/'.$file->getMonogram());
+ ->replaceQueryParam('next', '/'.$file->getMonogram());
return id(new PHUIFormLayoutView())
->addClass('phui-comment-panel-empty')
->appendChild(
id(new PHUIButtonView())
->setTag('a')
->setText(pht('Log In to Comment'))
->setHref((string)$login_href));
}
$draft = PhabricatorDraft::newFromUserAndKey(
$viewer,
$file->getPHID());
$post_uri = $this->getApplicationURI('thread/'.$file->getPHID().'/');
$form = id(new AphrontFormView())
->setUser($viewer)
->setAction($post_uri)
->addSigil('lightbox-comment-form')
->addClass('lightbox-comment-form')
->setWorkflow(true)
->appendChild(
id(new PhabricatorRemarkupControl())
->setUser($viewer)
->setName('comment')
->setValue($draft->getDraft()))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Comment')));
$view = phutil_tag_div('phui-comment-panel', $form);
return $view;
}
}
diff --git a/src/applications/files/controller/PhabricatorFileTransformListController.php b/src/applications/files/controller/PhabricatorFileTransformListController.php
index ab5322fc1..7b5bc9299 100644
--- a/src/applications/files/controller/PhabricatorFileTransformListController.php
+++ b/src/applications/files/controller/PhabricatorFileTransformListController.php
@@ -1,147 +1,147 @@
<?php
final class PhabricatorFileTransformListController
extends PhabricatorFileController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->executeOne();
if (!$file) {
return new Aphront404Response();
}
$monogram = $file->getMonogram();
$xdst = id(new PhabricatorTransformedFile())->loadAllWhere(
'transformedPHID = %s',
$file->getPHID());
$dst_rows = array();
foreach ($xdst as $source) {
$dst_rows[] = array(
$source->getTransform(),
$viewer->renderHandle($source->getOriginalPHID()),
);
}
$dst_table = id(new AphrontTableView($dst_rows))
->setHeaders(
array(
pht('Key'),
pht('Source'),
))
->setColumnClasses(
array(
'',
'wide',
))
->setNoDataString(
pht(
'This file was not created by transforming another file.'));
$xsrc = id(new PhabricatorTransformedFile())->loadAllWhere(
'originalPHID = %s',
$file->getPHID());
$xsrc = mpull($xsrc, 'getTransformedPHID', 'getTransform');
$src_rows = array();
$xforms = PhabricatorFileTransform::getAllTransforms();
foreach ($xforms as $xform) {
$dst_phid = idx($xsrc, $xform->getTransformKey());
if ($xform->canApplyTransform($file)) {
$can_apply = pht('Yes');
$view_href = $file->getURIForTransform($xform);
$view_href = new PhutilURI($view_href);
- $view_href->setQueryParam('regenerate', 'true');
+ $view_href->replaceQueryParam('regenerate', 'true');
$view_text = pht('Regenerate');
$view_link = phutil_tag(
'a',
array(
'class' => 'small button button-grey',
'href' => $view_href,
),
$view_text);
} else {
$can_apply = phutil_tag('em', array(), pht('No'));
$view_link = phutil_tag('em', array(), pht('None'));
}
if ($dst_phid) {
$dst_link = $viewer->renderHandle($dst_phid);
} else {
$dst_link = phutil_tag('em', array(), pht('None'));
}
$src_rows[] = array(
$xform->getTransformName(),
$xform->getTransformKey(),
$can_apply,
$dst_link,
$view_link,
);
}
$src_table = id(new AphrontTableView($src_rows))
->setHeaders(
array(
pht('Name'),
pht('Key'),
pht('Supported'),
pht('Transform'),
pht('View'),
))
->setColumnClasses(
array(
'wide',
'',
'',
'',
'action',
));
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($monogram, '/'.$monogram);
$crumbs->addTextCrumb(pht('Transforms'));
$crumbs->setBorder(true);
$dst_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('File Sources'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($dst_table);
$src_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Available Transforms'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($src_table);
$title = pht('%s Transforms', $file->getName());
$header = id(new PHUIHeaderView())
->setHeader($title)
->setHeaderIcon('fa-arrows-alt');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(
$dst_box,
$src_box,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
}
diff --git a/src/applications/files/markup/PhabricatorImageRemarkupRule.php b/src/applications/files/markup/PhabricatorImageRemarkupRule.php
index 5d1979ed3..57ad75bbc 100644
--- a/src/applications/files/markup/PhabricatorImageRemarkupRule.php
+++ b/src/applications/files/markup/PhabricatorImageRemarkupRule.php
@@ -1,171 +1,171 @@
<?php
final class PhabricatorImageRemarkupRule extends PhutilRemarkupRule {
const KEY_RULE_EXTERNAL_IMAGE = 'rule.external-image';
public function getPriority() {
return 200.0;
}
public function apply($text) {
return preg_replace_callback(
'@{(image|img) ((?:[^}\\\\]+|\\\\.)*)}@m',
array($this, 'markupImage'),
$text);
}
public function markupImage(array $matches) {
if (!$this->isFlatText($matches[0])) {
return $matches[0];
}
$args = array();
$defaults = array(
'uri' => null,
'alt' => null,
'width' => null,
'height' => null,
);
$trimmed_match = trim($matches[2]);
if ($this->isURI($trimmed_match)) {
$args['uri'] = $trimmed_match;
} else {
$parser = new PhutilSimpleOptions();
$keys = $parser->parse($trimmed_match);
$uri_key = '';
foreach (array('src', 'uri', 'url') as $key) {
if (array_key_exists($key, $keys)) {
$uri_key = $key;
}
}
if ($uri_key) {
$args['uri'] = $keys[$uri_key];
}
$args += $keys;
}
$args += $defaults;
if (!strlen($args['uri'])) {
return $matches[0];
}
// Make sure this is something that looks roughly like a real URI. We'll
// validate it more carefully before proxying it, but if whatever the user
// has typed isn't even close, just decline to activate the rule behavior.
try {
$uri = new PhutilURI($args['uri']);
if (!strlen($uri->getProtocol())) {
return $matches[0];
}
$args['uri'] = (string)$uri;
} catch (Exception $ex) {
return $matches[0];
}
$engine = $this->getEngine();
$metadata_key = self::KEY_RULE_EXTERNAL_IMAGE;
$metadata = $engine->getTextMetadata($metadata_key, array());
$token = $engine->storeText('<img>');
$metadata[] = array(
'token' => $token,
'args' => $args,
);
$engine->setTextMetadata($metadata_key, $metadata);
return $token;
}
public function didMarkupText() {
$engine = $this->getEngine();
$metadata_key = self::KEY_RULE_EXTERNAL_IMAGE;
$images = $engine->getTextMetadata($metadata_key, array());
$engine->setTextMetadata($metadata_key, array());
if (!$images) {
return;
}
// Look for images we've already successfully fetched that aren't about
// to get eaten by the GC. For any we find, we can just emit a normal
// "<img />" tag pointing directly to the file.
// For files which we don't hit in the cache, we emit a placeholder
// instead and use AJAX to actually perform the fetch.
$digests = array();
foreach ($images as $image) {
$uri = $image['args']['uri'];
$digests[] = PhabricatorHash::digestForIndex($uri);
}
$caches = id(new PhabricatorFileExternalRequest())->loadAllWhere(
'uriIndex IN (%Ls) AND isSuccessful = 1 AND ttl > %d',
$digests,
PhabricatorTime::getNow() + phutil_units('1 hour in seconds'));
$file_phids = array();
foreach ($caches as $cache) {
$file_phids[$cache->getFilePHID()] = $cache->getURI();
}
$file_map = array();
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array_keys($file_phids))
->execute();
foreach ($files as $file) {
$phid = $file->getPHID();
$file_remote_uri = $file_phids[$phid];
$file_view_uri = $file->getViewURI();
$file_map[$file_remote_uri] = $file_view_uri;
}
}
foreach ($images as $image) {
$args = $image['args'];
$uri = $args['uri'];
$direct_uri = idx($file_map, $uri);
if ($direct_uri) {
$img = phutil_tag(
'img',
array(
'src' => $direct_uri,
'alt' => $args['alt'],
'width' => $args['width'],
'height' => $args['height'],
));
} else {
$src_uri = id(new PhutilURI('/file/imageproxy/'))
- ->setQueryParam('uri', $uri);
+ ->replaceQueryParam('uri', $uri);
$img = id(new PHUIRemarkupImageView())
->setURI($src_uri)
->setAlt($args['alt'])
->setWidth($args['width'])
->setHeight($args['height']);
}
$engine->overwriteStoredText($image['token'], $img);
}
}
private function isURI($uri_string) {
// Very simple check to make sure it starts with either http or https.
// If it does, we'll try to treat it like a valid URI
return preg_match('~^https?\:\/\/.*\z~i', $uri_string);
}
}
diff --git a/src/applications/fund/storage/FundBacker.php b/src/applications/fund/storage/FundBacker.php
index 87ab342e2..ebdf39ae1 100644
--- a/src/applications/fund/storage/FundBacker.php
+++ b/src/applications/fund/storage/FundBacker.php
@@ -1,117 +1,120 @@
<?php
final class FundBacker extends FundDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface {
protected $initiativePHID;
protected $backerPHID;
protected $amountAsCurrency;
protected $status;
protected $properties = array();
private $initiative = self::ATTACHABLE;
const STATUS_NEW = 'new';
const STATUS_IN_CART = 'in-cart';
const STATUS_PURCHASED = 'purchased';
public static function initializeNewBacker(PhabricatorUser $actor) {
return id(new FundBacker())
->setBackerPHID($actor->getPHID())
->setStatus(self::STATUS_NEW);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_APPLICATION_SERIALIZERS => array(
'amountAsCurrency' => new PhortuneCurrencySerializer(),
),
self::CONFIG_COLUMN_SCHEMA => array(
'status' => 'text32',
'amountAsCurrency' => 'text64',
),
self::CONFIG_KEY_SCHEMA => array(
'key_initiative' => array(
'columns' => array('initiativePHID'),
),
'key_backer' => array(
'columns' => array('backerPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(FundBackerPHIDType::TYPECONST);
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getInitiative() {
return $this->assertAttached($this->initiative);
}
public function attachInitiative(FundInitiative $initiative = null) {
$this->initiative = $initiative;
return $this;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
// If we have the initiative, use the initiative's policy.
// Otherwise, return NOONE. This allows the backer to continue seeing
// a backer even if they're no longer allowed to see the initiative.
$initiative = $this->getInitiative();
if ($initiative) {
return $initiative->getPolicy($capability);
}
return PhabricatorPolicies::POLICY_NOONE;
+ case PhabricatorPolicyCapability::CAN_EDIT:
+ return PhabricatorPolicies::POLICY_NOONE;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return ($viewer->getPHID() == $this->getBackerPHID());
}
public function describeAutomaticCapability($capability) {
return pht('A backer can always see what they have backed.');
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new FundBackerEditor();
}
public function getApplicationTransactionTemplate() {
return new FundBackerTransaction();
}
}
diff --git a/src/applications/fund/storage/FundBackerTransaction.php b/src/applications/fund/storage/FundBackerTransaction.php
index c24e769eb..abaf585ae 100644
--- a/src/applications/fund/storage/FundBackerTransaction.php
+++ b/src/applications/fund/storage/FundBackerTransaction.php
@@ -1,22 +1,22 @@
<?php
final class FundBackerTransaction
extends PhabricatorModularTransaction {
public function getApplicationName() {
return 'fund';
}
public function getApplicationTransactionType() {
return FundBackerPHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return null;
+ public function getBaseTransactionClass() {
+ return 'FundBackerTransactionType';
}
public function getBaseTransactionClass() {
return 'FundBackerTransactionType';
}
}
diff --git a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php
index 80be90b37..4b369e821 100644
--- a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php
+++ b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php
@@ -1,128 +1,130 @@
<?php
final class PhabricatorHarbormasterApplication extends PhabricatorApplication {
public function getBaseURI() {
return '/harbormaster/';
}
public function getName() {
return pht('Harbormaster');
}
public function getShortDescription() {
return pht('Build/CI');
}
public function getIcon() {
return 'fa-ship';
}
public function getTitleGlyph() {
return "\xE2\x99\xBB";
}
public function getFlavorText() {
return pht('Ship Some Freight');
}
public function getApplicationGroup() {
return self::GROUP_UTILITIES;
}
public function getEventListeners() {
return array(
new HarbormasterUIEventListener(),
);
}
public function getRemarkupRules() {
return array(
new HarbormasterRemarkupRule(),
);
}
public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
return array(
array(
'name' => pht('Harbormaster User Guide'),
'href' => PhabricatorEnv::getDoclink('Harbormaster User Guide'),
),
);
}
public function getRoutes() {
return array(
'/B(?P<id>[1-9]\d*)' => 'HarbormasterBuildableViewController',
'/harbormaster/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?'
=> 'HarbormasterBuildableListController',
'step/' => array(
'add/(?:(?P<id>\d+)/)?' => 'HarbormasterStepAddController',
'new/(?P<plan>\d+)/(?P<class>[^/]+)/'
=> 'HarbormasterStepEditController',
'view/(?P<id>\d+)/' => 'HarbormasterStepViewController',
'edit/(?:(?P<id>\d+)/)?' => 'HarbormasterStepEditController',
'delete/(?:(?P<id>\d+)/)?' => 'HarbormasterStepDeleteController',
),
'buildable/' => array(
'(?P<id>\d+)/(?P<action>pause|resume|restart|abort)/'
=> 'HarbormasterBuildableActionController',
),
'build/' => array(
$this->getQueryRoutePattern() => 'HarbormasterBuildListController',
'(?P<id>\d+)/(?:(?P<generation>\d+)/)?'
=> 'HarbormasterBuildViewController',
'(?P<action>pause|resume|restart|abort)/'.
'(?P<id>\d+)/(?:(?P<via>[^/]+)/)?'
=> 'HarbormasterBuildActionController',
),
'plan/' => array(
$this->getQueryRoutePattern() => 'HarbormasterPlanListController',
$this->getEditRoutePattern('edit/')
=> 'HarbormasterPlanEditController',
'order/(?:(?P<id>\d+)/)?' => 'HarbormasterPlanOrderController',
'disable/(?P<id>\d+)/' => 'HarbormasterPlanDisableController',
+ 'behavior/(?P<id>\d+)/(?P<behaviorKey>[^/]+)/' =>
+ 'HarbormasterPlanBehaviorController',
'run/(?P<id>\d+)/' => 'HarbormasterPlanRunController',
'(?P<id>\d+)/' => 'HarbormasterPlanViewController',
),
'unit/' => array(
'(?P<id>\d+)/' => 'HarbormasterUnitMessageListController',
'view/(?P<id>\d+)/' => 'HarbormasterUnitMessageViewController',
),
'lint/' => array(
'(?P<id>\d+)/' => 'HarbormasterLintMessagesController',
),
'hook/' => array(
'circleci/' => 'HarbormasterCircleCIHookController',
'buildkite/' => 'HarbormasterBuildkiteHookController',
),
'log/' => array(
'view/(?P<id>\d+)/(?:\$(?P<lines>\d+(?:-\d+)?))?'
=> 'HarbormasterBuildLogViewController',
'render/(?P<id>\d+)/(?:\$(?P<lines>\d+(?:-\d+)?))?'
=> 'HarbormasterBuildLogRenderController',
'download/(?P<id>\d+)/' => 'HarbormasterBuildLogDownloadController',
),
),
);
}
protected function getCustomCapabilities() {
return array(
HarbormasterCreatePlansCapability::CAPABILITY => array(
'default' => PhabricatorPolicies::POLICY_ADMIN,
),
HarbormasterBuildPlanDefaultViewCapability::CAPABILITY => array(
'template' => HarbormasterBuildPlanPHIDType::TYPECONST,
'capability' => PhabricatorPolicyCapability::CAN_VIEW,
),
HarbormasterBuildPlanDefaultEditCapability::CAPABILITY => array(
'template' => HarbormasterBuildPlanPHIDType::TYPECONST,
'capability' => PhabricatorPolicyCapability::CAN_EDIT,
'default' => PhabricatorPolicies::POLICY_ADMIN,
),
);
}
}
diff --git a/src/applications/harbormaster/codex/HarbormasterBuildPlanPolicyCodex.php b/src/applications/harbormaster/codex/HarbormasterBuildPlanPolicyCodex.php
new file mode 100644
index 000000000..a17f2fb29
--- /dev/null
+++ b/src/applications/harbormaster/codex/HarbormasterBuildPlanPolicyCodex.php
@@ -0,0 +1,38 @@
+<?php
+
+final class HarbormasterBuildPlanPolicyCodex
+ extends PhabricatorPolicyCodex {
+
+ public function getPolicySpecialRuleDescriptions() {
+ $object = $this->getObject();
+ $run_with_view = $object->canRunWithoutEditCapability();
+
+ $rules = array();
+
+ $rules[] = $this->newRule()
+ ->setCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->setIsActive(!$run_with_view)
+ ->setDescription(
+ pht(
+ 'You must have edit permission on this build plan to pause, '.
+ 'abort, resume, or restart it.'));
+
+ $rules[] = $this->newRule()
+ ->setCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->setIsActive(!$run_with_view)
+ ->setDescription(
+ pht(
+ 'You must have edit permission on this build plan to run it '.
+ 'manually.'));
+
+ return $rules;
+ }
+
+
+}
diff --git a/src/applications/harbormaster/conduit/HarbormasterBuildPlanEditAPIMethod.php b/src/applications/harbormaster/conduit/HarbormasterBuildPlanEditAPIMethod.php
new file mode 100644
index 000000000..5509cf189
--- /dev/null
+++ b/src/applications/harbormaster/conduit/HarbormasterBuildPlanEditAPIMethod.php
@@ -0,0 +1,20 @@
+<?php
+
+final class HarbormasterBuildPlanEditAPIMethod
+ extends PhabricatorEditEngineAPIMethod {
+
+ public function getAPIMethodName() {
+ return 'harbormaster.buildplan.edit';
+ }
+
+ public function newEditEngine() {
+ return new HarbormasterBuildPlanEditEngine();
+ }
+
+ public function getMethodSummary() {
+ return pht(
+ 'Apply transactions to create a new build plan or edit an existing '.
+ 'one.');
+ }
+
+}
diff --git a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php
index 7437e48f6..3ceb8e068 100644
--- a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php
+++ b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php
@@ -1,217 +1,221 @@
<?php
final class HarbormasterBuildStatus extends Phobject {
const STATUS_INACTIVE = 'inactive';
const STATUS_PENDING = 'pending';
const STATUS_BUILDING = 'building';
const STATUS_PASSED = 'passed';
const STATUS_FAILED = 'failed';
const STATUS_ABORTED = 'aborted';
const STATUS_ERROR = 'error';
const STATUS_PAUSED = 'paused';
const STATUS_DEADLOCKED = 'deadlocked';
private $key;
private $properties;
public function __construct($key, array $properties) {
$this->key = $key;
$this->properties = $properties;
}
public static function newBuildStatusObject($status) {
$spec = self::getBuildStatusSpec($status);
return new self($status, $spec);
}
private function getProperty($key) {
if (!array_key_exists($key, $this->properties)) {
throw new Exception(
pht(
'Attempting to access unknown build status property ("%s").',
$key));
}
return $this->properties[$key];
}
public function isBuilding() {
return $this->getProperty('isBuilding');
}
public function isPaused() {
return ($this->key === self::STATUS_PAUSED);
}
public function isComplete() {
return $this->getProperty('isComplete');
}
public function isPassed() {
return ($this->key === self::STATUS_PASSED);
}
+ public function isFailed() {
+ return ($this->key === self::STATUS_FAILED);
+ }
+
/**
* Get a human readable name for a build status constant.
*
* @param const Build status constant.
* @return string Human-readable name.
*/
public static function getBuildStatusName($status) {
$spec = self::getBuildStatusSpec($status);
return $spec['name'];
}
public static function getBuildStatusMap() {
$specs = self::getBuildStatusSpecMap();
return ipull($specs, 'name');
}
public static function getBuildStatusIcon($status) {
$spec = self::getBuildStatusSpec($status);
return $spec['icon'];
}
public static function getBuildStatusColor($status) {
$spec = self::getBuildStatusSpec($status);
return $spec['color'];
}
public static function getBuildStatusANSIColor($status) {
$spec = self::getBuildStatusSpec($status);
return $spec['color.ansi'];
}
public static function getWaitingStatusConstants() {
return array(
self::STATUS_INACTIVE,
self::STATUS_PENDING,
);
}
public static function getActiveStatusConstants() {
return array(
self::STATUS_BUILDING,
self::STATUS_PAUSED,
);
}
public static function getIncompleteStatusConstants() {
$map = self::getBuildStatusSpecMap();
$constants = array();
foreach ($map as $constant => $spec) {
if (!$spec['isComplete']) {
$constants[] = $constant;
}
}
return $constants;
}
public static function getCompletedStatusConstants() {
return array(
self::STATUS_PASSED,
self::STATUS_FAILED,
self::STATUS_ABORTED,
self::STATUS_ERROR,
self::STATUS_DEADLOCKED,
);
}
private static function getBuildStatusSpecMap() {
return array(
self::STATUS_INACTIVE => array(
'name' => pht('Inactive'),
'icon' => 'fa-circle-o',
'color' => 'dark',
'color.ansi' => 'yellow',
'isBuilding' => false,
'isComplete' => false,
),
self::STATUS_PENDING => array(
'name' => pht('Pending'),
'icon' => 'fa-circle-o',
'color' => 'blue',
'color.ansi' => 'yellow',
'isBuilding' => true,
'isComplete' => false,
),
self::STATUS_BUILDING => array(
'name' => pht('Building'),
'icon' => 'fa-chevron-circle-right',
'color' => 'blue',
'color.ansi' => 'yellow',
'isBuilding' => true,
'isComplete' => false,
),
self::STATUS_PASSED => array(
'name' => pht('Passed'),
'icon' => 'fa-check-circle',
'color' => 'green',
'color.ansi' => 'green',
'isBuilding' => false,
'isComplete' => true,
),
self::STATUS_FAILED => array(
'name' => pht('Failed'),
'icon' => 'fa-times-circle',
'color' => 'red',
'color.ansi' => 'red',
'isBuilding' => false,
'isComplete' => true,
),
self::STATUS_ABORTED => array(
'name' => pht('Aborted'),
'icon' => 'fa-minus-circle',
'color' => 'red',
'color.ansi' => 'red',
'isBuilding' => false,
'isComplete' => true,
),
self::STATUS_ERROR => array(
'name' => pht('Unexpected Error'),
'icon' => 'fa-minus-circle',
'color' => 'red',
'color.ansi' => 'red',
'isBuilding' => false,
'isComplete' => true,
),
self::STATUS_PAUSED => array(
'name' => pht('Paused'),
'icon' => 'fa-minus-circle',
'color' => 'dark',
'color.ansi' => 'yellow',
'isBuilding' => false,
'isComplete' => false,
),
self::STATUS_DEADLOCKED => array(
'name' => pht('Deadlocked'),
'icon' => 'fa-exclamation-circle',
'color' => 'red',
'color.ansi' => 'red',
'isBuilding' => false,
'isComplete' => true,
),
);
}
private static function getBuildStatusSpec($status) {
$map = self::getBuildStatusSpecMap();
if (isset($map[$status])) {
return $map[$status];
}
return array(
'name' => pht('Unknown ("%s")', $status),
'icon' => 'fa-question-circle',
'color' => 'bluegrey',
'color.ansi' => 'magenta',
'isBuilding' => false,
'isComplete' => false,
);
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php
index 843ffd470..6a4a2b1fe 100644
--- a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php
@@ -1,151 +1,149 @@
<?php
final class HarbormasterBuildActionController
extends HarbormasterController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$action = $request->getURIData('action');
$via = $request->getURIData('via');
$build = id(new HarbormasterBuildQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$build) {
return new Aphront404Response();
}
switch ($action) {
case HarbormasterBuildCommand::COMMAND_RESTART:
$can_issue = $build->canRestartBuild();
break;
case HarbormasterBuildCommand::COMMAND_PAUSE:
$can_issue = $build->canPauseBuild();
break;
case HarbormasterBuildCommand::COMMAND_RESUME:
$can_issue = $build->canResumeBuild();
break;
case HarbormasterBuildCommand::COMMAND_ABORT:
$can_issue = $build->canAbortBuild();
break;
default:
return new Aphront400Response();
}
$build->assertCanIssueCommand($viewer, $action);
switch ($via) {
case 'buildable':
$return_uri = '/'.$build->getBuildable()->getMonogram();
break;
default:
$return_uri = $this->getApplicationURI('/build/'.$build->getID().'/');
break;
}
if ($request->isDialogFormPost() && $can_issue) {
$build->sendMessage($viewer, $action);
return id(new AphrontRedirectResponse())->setURI($return_uri);
}
switch ($action) {
case HarbormasterBuildCommand::COMMAND_RESTART:
if ($can_issue) {
$title = pht('Really restart build?');
$body = pht(
'Progress on this build will be discarded and the build will '.
'restart. Side effects of the build will occur again. Really '.
'restart build?');
$submit = pht('Restart Build');
} else {
- $title = pht('Unable to Restart Build');
- if ($build->isRestarting()) {
- $body = pht(
- 'This build is already restarting. You can not reissue a '.
- 'restart command to a restarting build.');
- } else {
- $body = pht('You can not restart this build.');
+ try {
+ $build->assertCanRestartBuild();
+ throw new Exception(pht('Expected to be unable to restart build.'));
+ } catch (HarbormasterRestartException $ex) {
+ $title = $ex->getTitle();
+ $body = $ex->getBody();
}
}
break;
case HarbormasterBuildCommand::COMMAND_ABORT:
if ($can_issue) {
$title = pht('Really abort build?');
$body = pht(
'Progress on this build will be discarded. Really '.
'abort build?');
$submit = pht('Abort Build');
} else {
$title = pht('Unable to Abort Build');
$body = pht('You can not abort this build.');
}
break;
case HarbormasterBuildCommand::COMMAND_PAUSE:
if ($can_issue) {
$title = pht('Really pause build?');
$body = pht(
'If you pause this build, work will halt once the current steps '.
'complete. You can resume the build later.');
$submit = pht('Pause Build');
} else {
$title = pht('Unable to Pause Build');
if ($build->isComplete()) {
$body = pht(
'This build is already complete. You can not pause a completed '.
'build.');
} else if ($build->isPaused()) {
$body = pht(
'This build is already paused. You can not pause a build which '.
'has already been paused.');
} else if ($build->isPausing()) {
$body = pht(
'This build is already pausing. You can not reissue a pause '.
'command to a pausing build.');
} else {
$body = pht(
'This build can not be paused.');
}
}
break;
case HarbormasterBuildCommand::COMMAND_RESUME:
if ($can_issue) {
$title = pht('Really resume build?');
$body = pht(
'Work will continue on the build. Really resume?');
$submit = pht('Resume Build');
} else {
$title = pht('Unable to Resume Build');
if ($build->isResuming()) {
$body = pht(
'This build is already resuming. You can not reissue a resume '.
'command to a resuming build.');
} else if (!$build->isPaused()) {
$body = pht(
'This build is not paused. You can only resume a paused '.
'build.');
}
}
break;
}
- $dialog = id(new AphrontDialogView())
- ->setUser($viewer)
+ $dialog = $this->newDialog()
->setTitle($title)
->appendChild($body)
->addCancelButton($return_uri);
if ($can_issue) {
$dialog->addSubmitButton($submit);
}
- return id(new AphrontDialogResponse())->setDialog($dialog);
+ return $dialog;
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php
index 1e79ad2b4..40f658711 100644
--- a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php
@@ -1,358 +1,359 @@
<?php
final class HarbormasterBuildableViewController
extends HarbormasterController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$buildable = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->executeOne();
if (!$buildable) {
return new Aphront404Response();
}
$id = $buildable->getID();
// Pull builds and build targets.
$builds = id(new HarbormasterBuildQuery())
->setViewer($viewer)
->withBuildablePHIDs(array($buildable->getPHID()))
->needBuildTargets(true)
->execute();
list($lint, $unit) = $this->renderLintAndUnit($buildable, $builds);
$buildable->attachBuilds($builds);
$object = $buildable->getBuildableObject();
$build_list = $this->buildBuildList($buildable);
$title = pht('Buildable %d', $id);
$header = id(new PHUIHeaderView())
->setHeader($title)
->setUser($viewer)
->setPolicyObject($buildable)
->setStatus(
$buildable->getStatusIcon(),
$buildable->getStatusColor(),
$buildable->getStatusDisplayName())
->setHeaderIcon('fa-recycle');
$timeline = $this->buildTransactionTimeline(
$buildable,
new HarbormasterBuildableTransactionQuery());
$timeline->setShouldTerminate(true);
$curtain = $this->buildCurtainView($buildable);
$properties = $this->buildPropertyList($buildable);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($buildable->getMonogram());
$crumbs->setBorder(true);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(array(
$properties,
$lint,
$unit,
$build_list,
$timeline,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
private function buildCurtainView(HarbormasterBuildable $buildable) {
$viewer = $this->getViewer();
$id = $buildable->getID();
$curtain = $this->newCurtainView($buildable);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$buildable,
PhabricatorPolicyCapability::CAN_EDIT);
$can_restart = false;
$can_resume = false;
$can_pause = false;
$can_abort = false;
$command_restart = HarbormasterBuildCommand::COMMAND_RESTART;
$command_resume = HarbormasterBuildCommand::COMMAND_RESUME;
$command_pause = HarbormasterBuildCommand::COMMAND_PAUSE;
$command_abort = HarbormasterBuildCommand::COMMAND_ABORT;
foreach ($buildable->getBuilds() as $build) {
if ($build->canRestartBuild()) {
if ($build->canIssueCommand($viewer, $command_restart)) {
$can_restart = true;
}
}
if ($build->canResumeBuild()) {
if ($build->canIssueCommand($viewer, $command_resume)) {
$can_resume = true;
}
}
if ($build->canPauseBuild()) {
if ($build->canIssueCommand($viewer, $command_pause)) {
$can_pause = true;
}
}
if ($build->canAbortBuild()) {
if ($build->canIssueCommand($viewer, $command_abort)) {
$can_abort = true;
}
}
}
$restart_uri = "buildable/{$id}/restart/";
$pause_uri = "buildable/{$id}/pause/";
$resume_uri = "buildable/{$id}/resume/";
$abort_uri = "buildable/{$id}/abort/";
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-repeat')
->setName(pht('Restart All Builds'))
->setHref($this->getApplicationURI($restart_uri))
->setWorkflow(true)
->setDisabled(!$can_restart || !$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-pause')
->setName(pht('Pause All Builds'))
->setHref($this->getApplicationURI($pause_uri))
->setWorkflow(true)
->setDisabled(!$can_pause || !$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-play')
->setName(pht('Resume All Builds'))
->setHref($this->getApplicationURI($resume_uri))
->setWorkflow(true)
->setDisabled(!$can_resume || !$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-exclamation-triangle')
->setName(pht('Abort All Builds'))
->setHref($this->getApplicationURI($abort_uri))
->setWorkflow(true)
->setDisabled(!$can_abort || !$can_edit));
return $curtain;
}
private function buildPropertyList(HarbormasterBuildable $buildable) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer);
$container_phid = $buildable->getContainerPHID();
$buildable_phid = $buildable->getBuildablePHID();
if ($container_phid) {
$properties->addProperty(
pht('Container'),
$viewer->renderHandle($container_phid));
}
$properties->addProperty(
pht('Buildable'),
$viewer->renderHandle($buildable_phid));
$properties->addProperty(
pht('Origin'),
$buildable->getIsManualBuildable()
? pht('Manual Buildable')
: pht('Automatic Buildable'));
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Properties'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($properties);
}
private function buildBuildList(HarbormasterBuildable $buildable) {
$viewer = $this->getRequest()->getUser();
$build_list = id(new PHUIObjectItemListView())
->setUser($viewer);
foreach ($buildable->getBuilds() as $build) {
$view_uri = $this->getApplicationURI('/build/'.$build->getID().'/');
$item = id(new PHUIObjectItemView())
->setObjectName(pht('Build %d', $build->getID()))
->setHeader($build->getName())
->setHref($view_uri);
$status = $build->getBuildStatus();
$status_color = HarbormasterBuildStatus::getBuildStatusColor($status);
$status_name = HarbormasterBuildStatus::getBuildStatusName($status);
$item->setStatusIcon('fa-dot-circle-o '.$status_color, $status_name);
$item->addAttribute($status_name);
if ($build->isRestarting()) {
$item->addIcon('fa-repeat', pht('Restarting'));
} else if ($build->isPausing()) {
$item->addIcon('fa-pause', pht('Pausing'));
} else if ($build->isResuming()) {
$item->addIcon('fa-play', pht('Resuming'));
}
$build_id = $build->getID();
$restart_uri = "build/restart/{$build_id}/buildable/";
$resume_uri = "build/resume/{$build_id}/buildable/";
$pause_uri = "build/pause/{$build_id}/buildable/";
$abort_uri = "build/abort/{$build_id}/buildable/";
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-repeat')
->setName(pht('Restart'))
->setHref($this->getApplicationURI($restart_uri))
->setWorkflow(true)
->setDisabled(!$build->canRestartBuild()));
if ($build->canResumeBuild()) {
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-play')
->setName(pht('Resume'))
->setHref($this->getApplicationURI($resume_uri))
->setWorkflow(true));
} else {
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-pause')
->setName(pht('Pause'))
->setHref($this->getApplicationURI($pause_uri))
->setWorkflow(true)
->setDisabled(!$build->canPauseBuild()));
}
$targets = $build->getBuildTargets();
if ($targets) {
$target_list = id(new PHUIStatusListView());
foreach ($targets as $target) {
$status = $target->getTargetStatus();
$icon = HarbormasterBuildTarget::getBuildTargetStatusIcon($status);
$color = HarbormasterBuildTarget::getBuildTargetStatusColor($status);
$status_name =
HarbormasterBuildTarget::getBuildTargetStatusName($status);
$name = $target->getName();
$target_list->addItem(
id(new PHUIStatusItemView())
->setIcon($icon, $color, $status_name)
->setTarget(pht('Target %d', $target->getID()))
->setNote($name));
}
$target_box = id(new PHUIBoxView())
->addPadding(PHUI::PADDING_SMALL)
->appendChild($target_list);
$item->appendChild($target_box);
}
$build_list->addItem($item);
}
$build_list->setFlush(true);
$box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Builds'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($build_list);
return $box;
}
private function renderLintAndUnit(
HarbormasterBuildable $buildable,
array $builds) {
$viewer = $this->getViewer();
$targets = array();
foreach ($builds as $build) {
foreach ($build->getBuildTargets() as $target) {
$targets[] = $target;
}
}
if (!$targets) {
return;
}
$target_phids = mpull($targets, 'getPHID');
$lint_data = id(new HarbormasterBuildLintMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls)',
$target_phids);
- $unit_data = id(new HarbormasterBuildUnitMessage())->loadAllWhere(
- 'buildTargetPHID IN (%Ls)',
- $target_phids);
+ $unit_data = id(new HarbormasterBuildUnitMessageQuery())
+ ->setViewer($viewer)
+ ->withBuildTargetPHIDs($target_phids)
+ ->execute();
if ($lint_data) {
$lint_table = id(new HarbormasterLintPropertyView())
->setViewer($viewer)
->setLimit(10)
->setLintMessages($lint_data);
$lint_href = $this->getApplicationURI('lint/'.$buildable->getID().'/');
$lint_header = id(new PHUIHeaderView())
->setHeader(pht('Lint Messages'))
->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setHref($lint_href)
->setIcon('fa-list-ul')
->setText('View All'));
$lint = id(new PHUIObjectBoxView())
->setHeader($lint_header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($lint_table);
} else {
$lint = null;
}
if ($unit_data) {
$unit = id(new HarbormasterUnitSummaryView())
->setViewer($viewer)
->setBuildable($buildable)
->setUnitMessages($unit_data)
->setShowViewAll(true)
->setLimit(5);
} else {
$unit = null;
}
return array($lint, $unit);
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterPlanBehaviorController.php b/src/applications/harbormaster/controller/HarbormasterPlanBehaviorController.php
new file mode 100644
index 000000000..8f1fece69
--- /dev/null
+++ b/src/applications/harbormaster/controller/HarbormasterPlanBehaviorController.php
@@ -0,0 +1,92 @@
+<?php
+
+final class HarbormasterPlanBehaviorController
+ extends HarbormasterPlanController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+
+ $plan = id(new HarbormasterBuildPlanQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($request->getURIData('id')))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+ if (!$plan) {
+ return new Aphront404Response();
+ }
+
+ $behavior_key = $request->getURIData('behaviorKey');
+ $metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey();
+
+ $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
+ $behavior = idx($behaviors, $behavior_key);
+ if (!$behavior) {
+ return new Aphront404Response();
+ }
+
+ $plan_uri = $plan->getURI();
+
+ $v_option = $behavior->getPlanOption($plan)->getKey();
+ if ($request->isFormPost()) {
+ $v_option = $request->getStr('option');
+
+ $xactions = array();
+
+ $xactions[] = id(new HarbormasterBuildPlanTransaction())
+ ->setTransactionType(
+ HarbormasterBuildPlanBehaviorTransaction::TRANSACTIONTYPE)
+ ->setMetadataValue($metadata_key, $behavior_key)
+ ->setNewValue($v_option);
+
+ $editor = id(new HarbormasterBuildPlanEditor())
+ ->setActor($viewer)
+ ->setContinueOnNoEffect(true)
+ ->setContinueOnMissingFields(true)
+ ->setContentSourceFromRequest($request);
+
+ $editor->applyTransactions($plan, $xactions);
+
+ return id(new AphrontRedirectResponse())->setURI($plan_uri);
+ }
+
+ $select_control = id(new AphrontFormRadioButtonControl())
+ ->setName('option')
+ ->setValue($v_option)
+ ->setLabel(pht('Option'));
+
+ foreach ($behavior->getOptions() as $option) {
+ $icon = id(new PHUIIconView())
+ ->setIcon($option->getIcon());
+
+ $select_control->addButton(
+ $option->getKey(),
+ array(
+ $icon,
+ ' ',
+ $option->getName(),
+ ),
+ $option->getDescription());
+ }
+
+ $form = id(new AphrontFormView())
+ ->setViewer($viewer)
+ ->appendInstructions(
+ pht(
+ 'Choose a build plan behavior for "%s".',
+ phutil_tag('strong', array(), $behavior->getName())))
+ ->appendRemarkupInstructions($behavior->getEditInstructions())
+ ->appendControl($select_control);
+
+ return $this->newDialog()
+ ->setTitle(pht('Edit Behavior: %s', $behavior->getName()))
+ ->appendForm($form)
+ ->setWidth(AphrontDialogView::WIDTH_FORM)
+ ->addSubmitButton(pht('Save Changes'))
+ ->addCancelButton($plan_uri);
+ }
+
+}
diff --git a/src/applications/harbormaster/controller/HarbormasterPlanDisableController.php b/src/applications/harbormaster/controller/HarbormasterPlanDisableController.php
index ccf6b8986..65a993396 100644
--- a/src/applications/harbormaster/controller/HarbormasterPlanDisableController.php
+++ b/src/applications/harbormaster/controller/HarbormasterPlanDisableController.php
@@ -1,68 +1,68 @@
<?php
final class HarbormasterPlanDisableController
extends HarbormasterPlanController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$plan = id(new HarbormasterBuildPlanQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$plan) {
return new Aphront404Response();
}
- $plan_uri = $this->getApplicationURI('plan/'.$plan->getID().'/');
+ $plan_uri = $plan->getURI();
if ($request->isFormPost()) {
- $type_status = HarbormasterBuildPlanTransaction::TYPE_STATUS;
+ $type_status = HarbormasterBuildPlanStatusTransaction::TRANSACTIONTYPE;
$v_status = $plan->isDisabled()
? HarbormasterBuildPlan::STATUS_ACTIVE
: HarbormasterBuildPlan::STATUS_DISABLED;
$xactions = array();
$xactions[] = id(new HarbormasterBuildPlanTransaction())
->setTransactionType($type_status)
->setNewValue($v_status);
$editor = id(new HarbormasterBuildPlanEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setContentSourceFromRequest($request);
$editor->applyTransactions($plan, $xactions);
return id(new AphrontRedirectResponse())->setURI($plan_uri);
}
if ($plan->isDisabled()) {
$title = pht('Enable Build Plan');
$body = pht('Enable this build plan?');
$button = pht('Enable Plan');
} else {
$title = pht('Disable Build Plan');
$body = pht(
'Disable this build plan? It will no longer be executed '.
'automatically.');
$button = pht('Disable Plan');
}
return $this->newDialog()
->setTitle($title)
->appendChild($body)
->addSubmitButton($button)
->addCancelButton($plan_uri);
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterPlanRunController.php b/src/applications/harbormaster/controller/HarbormasterPlanRunController.php
index fd227ee55..5d80d421a 100644
--- a/src/applications/harbormaster/controller/HarbormasterPlanRunController.php
+++ b/src/applications/harbormaster/controller/HarbormasterPlanRunController.php
@@ -1,107 +1,104 @@
<?php
final class HarbormasterPlanRunController extends HarbormasterPlanController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$plan_id = $request->getURIData('id');
$plan = id(new HarbormasterBuildPlanQuery())
->setViewer($viewer)
->withIDs(array($plan_id))
- ->requireCapabilities(
- array(
- PhabricatorPolicyCapability::CAN_VIEW,
- PhabricatorPolicyCapability::CAN_EDIT,
- ))
->executeOne();
if (!$plan) {
return new Aphront404Response();
}
+ $plan->assertHasRunCapability($viewer);
+
$cancel_uri = $this->getApplicationURI("plan/{$plan_id}/");
if (!$plan->canRunManually()) {
return $this->newDialog()
->setTitle(pht('Can Not Run Plan'))
->appendParagraph(pht('This plan can not be run manually.'))
->addCancelButton($cancel_uri);
}
$e_name = true;
$v_name = null;
$errors = array();
if ($request->isFormPost()) {
$buildable = HarbormasterBuildable::initializeNewBuildable($viewer)
->setIsManualBuildable(true);
$v_name = $request->getStr('buildablePHID');
if ($v_name) {
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withNames(array($v_name))
->executeOne();
if ($object instanceof HarbormasterBuildableInterface) {
$buildable
->setBuildablePHID($object->getHarbormasterBuildablePHID())
->setContainerPHID($object->getHarbormasterContainerPHID());
} else {
$e_name = pht('Invalid');
$errors[] = pht('Enter the name of a revision or commit.');
}
} else {
$e_name = pht('Required');
$errors[] = pht('You must choose a revision or commit to build.');
}
if (!$errors) {
$buildable->save();
$buildable->sendMessage(
$viewer,
HarbormasterMessageType::BUILDABLE_BUILD,
false);
$buildable->applyPlan($plan, array(), $viewer->getPHID());
$buildable_uri = '/B'.$buildable->getID();
return id(new AphrontRedirectResponse())->setURI($buildable_uri);
}
}
if ($errors) {
$errors = id(new PHUIInfoView())->setErrors($errors);
}
$title = pht('Run Build Plan Manually');
$save_button = pht('Run Plan Manually');
$form = id(new PHUIFormLayoutView())
->setUser($viewer)
->appendRemarkupInstructions(
pht(
"Enter the name of a commit or revision to run this plan on (for ".
"example, `rX123456` or `D123`).\n\n".
"For more detailed output, you can also run manual builds from ".
"the command line:\n\n".
" phabricator/ $ ./bin/harbormaster build <object> --plan %s",
$plan->getID()))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Buildable Name'))
->setName('buildablePHID')
->setError($e_name)
->setValue($v_name));
return $this->newDialog()
->setWidth(AphrontDialogView::WIDTH_FULL)
->setTitle($title)
->appendChild($form)
->addCancelButton($cancel_uri)
->addSubmitButton($save_button);
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php
index 6ebadf7a6..4f2b70fca 100644
--- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php
@@ -1,444 +1,617 @@
<?php
final class HarbormasterPlanViewController extends HarbormasterPlanController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$plan = id(new HarbormasterBuildPlanQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$plan) {
return new Aphront404Response();
}
- $timeline = $this->buildTransactionTimeline(
- $plan,
- new HarbormasterBuildPlanTransactionQuery());
- $timeline->setShouldTerminate(true);
-
$title = $plan->getName();
$header = id(new PHUIHeaderView())
->setHeader($plan->getName())
->setUser($viewer)
->setPolicyObject($plan)
->setHeaderIcon('fa-ship');
$curtain = $this->buildCurtainView($plan);
- $crumbs = $this->buildApplicationCrumbs();
- $crumbs->addTextCrumb(pht('Plan %d', $id));
- $crumbs->setBorder(true);
+ $crumbs = $this->buildApplicationCrumbs()
+ ->addTextCrumb($plan->getObjectName())
+ ->setBorder(true);
- list($step_list, $has_any_conflicts, $would_deadlock) =
+ list($step_list, $has_any_conflicts, $would_deadlock, $steps) =
$this->buildStepList($plan);
$error = null;
- if ($would_deadlock) {
- $error = pht('This build plan will deadlock when executed, due to '.
- 'circular dependencies present in the build plan. '.
- 'Examine the step list and resolve the deadlock.');
+ if (!$steps) {
+ $error = pht(
+ 'This build plan does not have any build steps yet, so it will '.
+ 'not do anything when run.');
+ } else if ($would_deadlock) {
+ $error = pht(
+ 'This build plan will deadlock when executed, due to circular '.
+ 'dependencies present in the build plan. Examine the step list '.
+ 'and resolve the deadlock.');
} else if ($has_any_conflicts) {
// A deadlocking build will also cause all the artifacts to be
// invalid, so we just skip showing this message if that's the
// case.
- $error = pht('This build plan has conflicts in one or more build steps. '.
- 'Examine the step list and resolve the listed errors.');
+ $error = pht(
+ 'This build plan has conflicts in one or more build steps. '.
+ 'Examine the step list and resolve the listed errors.');
}
if ($error) {
$error = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->appendChild($error);
}
+ $builds_view = $this->newBuildsView($plan);
+ $options_view = $this->newOptionsView($plan);
+ $rules_view = $this->newRulesView($plan);
+
+ $timeline = $this->buildTransactionTimeline(
+ $plan,
+ new HarbormasterBuildPlanTransactionQuery());
+ $timeline->setShouldTerminate(true);
+
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
- ->setMainColumn(array(
- $error,
- $step_list,
- $timeline,
- ));
+ ->setMainColumn(
+ array(
+ $error,
+ $step_list,
+ $options_view,
+ $rules_view,
+ $builds_view,
+ $timeline,
+ ));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
+ ->setPageObjectPHIDs(array($plan->getPHID()))
->appendChild($view);
}
private function buildStepList(HarbormasterBuildPlan $plan) {
$viewer = $this->getViewer();
$run_order = HarbormasterBuildGraph::determineDependencyExecution($plan);
$steps = id(new HarbormasterBuildStepQuery())
->setViewer($viewer)
->withBuildPlanPHIDs(array($plan->getPHID()))
->execute();
$steps = mpull($steps, null, 'getPHID');
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$plan,
PhabricatorPolicyCapability::CAN_EDIT);
$step_list = id(new PHUIObjectItemListView())
->setUser($viewer)
->setNoDataString(
pht('This build plan does not have any build steps yet.'));
$i = 1;
$last_depth = 0;
$has_any_conflicts = false;
$is_deadlocking = false;
foreach ($run_order as $run_ref) {
$step = $steps[$run_ref['node']->getPHID()];
$depth = $run_ref['depth'] + 1;
if ($last_depth !== $depth) {
$last_depth = $depth;
$i = 1;
} else {
$i++;
}
$step_id = $step->getID();
$view_uri = $this->getApplicationURI("step/view/{$step_id}/");
$item = id(new PHUIObjectItemView())
->setObjectName(pht('Step %d.%d', $depth, $i))
->setHeader($step->getName())
->setHref($view_uri);
$step_list->addItem($item);
$implementation = null;
try {
$implementation = $step->getStepImplementation();
} catch (Exception $ex) {
// We can't initialize the implementation. This might be because
// it's been renamed or no longer exists.
$item
->setStatusIcon('fa-warning red')
->addAttribute(pht(
'This step has an invalid implementation (%s).',
$step->getClassName()));
continue;
}
$item->addAttribute($implementation->getDescription());
$item->setHref($view_uri);
$depends = $step->getStepImplementation()->getDependencies($step);
$inputs = $step->getStepImplementation()->getArtifactInputs();
$outputs = $step->getStepImplementation()->getArtifactOutputs();
$has_conflicts = false;
if ($depends || $inputs || $outputs) {
$available_artifacts =
HarbormasterBuildStepImplementation::getAvailableArtifacts(
$plan,
$step,
null);
$available_artifacts = ipull($available_artifacts, 'type');
list($depends_ui, $has_conflicts) = $this->buildDependsOnList(
$depends,
pht('Depends On'),
$steps);
list($inputs_ui, $has_conflicts) = $this->buildArtifactList(
$inputs,
'in',
pht('Input Artifacts'),
$available_artifacts);
list($outputs_ui) = $this->buildArtifactList(
$outputs,
'out',
pht('Output Artifacts'),
array());
$item->appendChild(
phutil_tag(
'div',
array(
'class' => 'harbormaster-artifact-io',
),
array(
$depends_ui,
$inputs_ui,
$outputs_ui,
)));
}
if ($has_conflicts) {
$has_any_conflicts = true;
$item->setStatusIcon('fa-warning red');
}
if ($run_ref['cycle']) {
$is_deadlocking = true;
}
if ($is_deadlocking) {
$item->setStatusIcon('fa-warning red');
}
}
$step_list->setFlush(true);
$plan_id = $plan->getID();
$header = id(new PHUIHeaderView())
->setHeader(pht('Build Steps'))
->addActionLink(
id(new PHUIButtonView())
->setText(pht('Add Build Step'))
->setHref($this->getApplicationURI("step/add/{$plan_id}/"))
->setTag('a')
->setIcon('fa-plus')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$step_box = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($step_list);
- return array($step_box, $has_any_conflicts, $is_deadlocking);
+ return array($step_box, $has_any_conflicts, $is_deadlocking, $steps);
}
private function buildCurtainView(HarbormasterBuildPlan $plan) {
$viewer = $this->getViewer();
$id = $plan->getID();
$curtain = $this->newCurtainView($plan);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$plan,
PhabricatorPolicyCapability::CAN_EDIT);
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Plan'))
->setHref($this->getApplicationURI("plan/edit/{$id}/"))
->setWorkflow(!$can_edit)
->setDisabled(!$can_edit)
->setIcon('fa-pencil'));
if ($plan->isDisabled()) {
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Enable Plan'))
->setHref($this->getApplicationURI("plan/disable/{$id}/"))
->setWorkflow(true)
->setDisabled(!$can_edit)
->setIcon('fa-check'));
} else {
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Disable Plan'))
->setHref($this->getApplicationURI("plan/disable/{$id}/"))
->setWorkflow(true)
->setDisabled(!$can_edit)
->setIcon('fa-ban'));
}
- $can_run = ($can_edit && $plan->canRunManually());
+ $can_run = ($plan->hasRunCapability($viewer) && $plan->canRunManually());
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Run Plan Manually'))
->setHref($this->getApplicationURI("plan/run/{$id}/"))
->setWorkflow(true)
->setDisabled(!$can_run)
->setIcon('fa-play-circle'));
- $curtain->addPanel(
- id(new PHUICurtainPanelView())
- ->setHeaderText(pht('Created'))
- ->appendChild(phabricator_datetime($plan->getDateCreated(), $viewer)));
-
return $curtain;
}
private function buildArtifactList(
array $artifacts,
$kind,
$name,
array $available_artifacts) {
$has_conflicts = false;
if (!$artifacts) {
return array(null, $has_conflicts);
}
$this->requireResource('harbormaster-css');
$header = phutil_tag(
'div',
array(
'class' => 'harbormaster-artifact-summary-header',
),
$name);
$is_input = ($kind == 'in');
$list = new PHUIStatusListView();
foreach ($artifacts as $artifact) {
$error = null;
$key = idx($artifact, 'key');
if (!strlen($key)) {
$bound = phutil_tag('em', array(), pht('(null)'));
if ($is_input) {
// This is an unbound input. For now, all inputs are always required.
$icon = PHUIStatusItemView::ICON_WARNING;
$color = 'red';
$icon_label = pht('Required Input');
$has_conflicts = true;
$error = pht('This input is required, but not configured.');
} else {
// This is an unnamed output. Outputs do not necessarily need to be
// named.
$icon = PHUIStatusItemView::ICON_OPEN;
$color = 'bluegrey';
$icon_label = pht('Unused Output');
}
} else {
$bound = phutil_tag('strong', array(), $key);
if ($is_input) {
if (isset($available_artifacts[$key])) {
if ($available_artifacts[$key] == idx($artifact, 'type')) {
$icon = PHUIStatusItemView::ICON_ACCEPT;
$color = 'green';
$icon_label = pht('Valid Input');
} else {
$icon = PHUIStatusItemView::ICON_WARNING;
$color = 'red';
$icon_label = pht('Bad Input Type');
$has_conflicts = true;
$error = pht(
'This input is bound to the wrong artifact type. It is bound '.
'to a "%s" artifact, but should be bound to a "%s" artifact.',
$available_artifacts[$key],
idx($artifact, 'type'));
}
} else {
$icon = PHUIStatusItemView::ICON_QUESTION;
$color = 'red';
$icon_label = pht('Unknown Input');
$has_conflicts = true;
$error = pht(
'This input is bound to an artifact ("%s") which does not exist '.
'at this stage in the build process.',
$key);
}
} else {
$icon = PHUIStatusItemView::ICON_DOWN;
$color = 'green';
$icon_label = pht('Valid Output');
}
}
if ($error) {
$note = array(
phutil_tag('strong', array(), pht('ERROR:')),
' ',
$error,
);
} else {
$note = $bound;
}
$list->addItem(
id(new PHUIStatusItemView())
->setIcon($icon, $color, $icon_label)
->setTarget($artifact['name'])
->setNote($note));
}
$ui = array(
$header,
$list,
);
return array($ui, $has_conflicts);
}
private function buildDependsOnList(
array $step_phids,
$name,
array $steps) {
$has_conflicts = false;
- if (count($step_phids) === 0) {
+ if (!$step_phids) {
return null;
}
$this->requireResource('harbormaster-css');
$steps = mpull($steps, null, 'getPHID');
$header = phutil_tag(
'div',
array(
'class' => 'harbormaster-artifact-summary-header',
),
$name);
$list = new PHUIStatusListView();
foreach ($step_phids as $step_phid) {
$error = null;
if (idx($steps, $step_phid) === null) {
$icon = PHUIStatusItemView::ICON_WARNING;
$color = 'red';
$icon_label = pht('Missing Dependency');
$has_conflicts = true;
$error = pht(
"This dependency specifies a build step which doesn't exist.");
} else {
$bound = phutil_tag(
'strong',
array(),
idx($steps, $step_phid)->getName());
$icon = PHUIStatusItemView::ICON_ACCEPT;
$color = 'green';
$icon_label = pht('Valid Input');
}
if ($error) {
$note = array(
phutil_tag('strong', array(), pht('ERROR:')),
' ',
$error,
);
} else {
$note = $bound;
}
$list->addItem(
id(new PHUIStatusItemView())
->setIcon($icon, $color, $icon_label)
->setTarget(pht('Build Step'))
->setNote($note));
}
$ui = array(
$header,
$list,
);
return array($ui, $has_conflicts);
}
+
+ private function newBuildsView(HarbormasterBuildPlan $plan) {
+ $viewer = $this->getViewer();
+
+ $limit = 10;
+ $builds = id(new HarbormasterBuildQuery())
+ ->setViewer($viewer)
+ ->withBuildPlanPHIDs(array($plan->getPHID()))
+ ->setLimit($limit + 1)
+ ->execute();
+
+ $more_results = (count($builds) > $limit);
+ $builds = array_slice($builds, 0, $limit);
+
+ $list = id(new HarbormasterBuildView())
+ ->setViewer($viewer)
+ ->setBuilds($builds)
+ ->newObjectList();
+
+ $list->setNoDataString(pht('No recent builds.'));
+
+ $more_href = new PhutilURI(
+ $this->getApplicationURI('/build/'),
+ array('plan' => $plan->getPHID()));
+
+ if ($more_results) {
+ $list->newTailButton()
+ ->setHref($more_href);
+ }
+
+ $more_link = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setIcon('fa-list-ul')
+ ->setText(pht('View All Builds'))
+ ->setHref($more_href);
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader(pht('Recent Builds'))
+ ->addActionLink($more_link);
+
+ return id(new PHUIObjectBoxView())
+ ->setHeader($header)
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->appendChild($list);
+ }
+
+ private function newRulesView(HarbormasterBuildPlan $plan) {
+ $viewer = $this->getViewer();
+
+ $limit = 10;
+ $rules = id(new HeraldRuleQuery())
+ ->setViewer($viewer)
+ ->withDisabled(false)
+ ->withAffectedObjectPHIDs(array($plan->getPHID()))
+ ->needValidateAuthors(true)
+ ->setLimit($limit + 1)
+ ->execute();
+
+ $more_results = (count($rules) > $limit);
+ $rules = array_slice($rules, 0, $limit);
+
+ $list = id(new HeraldRuleListView())
+ ->setViewer($viewer)
+ ->setRules($rules)
+ ->newObjectList();
+
+ $list->setNoDataString(pht('No active Herald rules trigger this build.'));
+
+ $more_href = new PhutilURI(
+ '/herald/',
+ array('affectedPHID' => $plan->getPHID()));
+
+ if ($more_results) {
+ $list->newTailButton()
+ ->setHref($more_href);
+ }
+
+ $more_link = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setIcon('fa-list-ul')
+ ->setText(pht('View All Rules'))
+ ->setHref($more_href);
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader(pht('Run By Herald Rules'))
+ ->addActionLink($more_link);
+
+ return id(new PHUIObjectBoxView())
+ ->setHeader($header)
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->appendChild($list);
+ }
+
+ private function newOptionsView(HarbormasterBuildPlan $plan) {
+ $viewer = $this->getViewer();
+
+ $can_edit = PhabricatorPolicyFilter::hasCapability(
+ $viewer,
+ $plan,
+ PhabricatorPolicyCapability::CAN_EDIT);
+
+ $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
+
+ $rows = array();
+ foreach ($behaviors as $behavior) {
+ $option = $behavior->getPlanOption($plan);
+
+ $icon = $option->getIcon();
+ $icon = id(new PHUIIconView())->setIcon($icon);
+
+ $edit_uri = new PhutilURI(
+ $this->getApplicationURI(
+ urisprintf(
+ 'plan/behavior/%d/%s/',
+ $plan->getID(),
+ $behavior->getKey())));
+
+ $edit_button = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setColor(PHUIButtonView::GREY)
+ ->setSize(PHUIButtonView::SMALL)
+ ->setDisabled(!$can_edit)
+ ->setWorkflow(true)
+ ->setText(pht('Edit'))
+ ->setHref($edit_uri);
+
+ $rows[] = array(
+ $icon,
+ $behavior->getName(),
+ $option->getName(),
+ $option->getDescription(),
+ $edit_button,
+ );
+ }
+
+ $table = id(new AphrontTableView($rows))
+ ->setHeaders(
+ array(
+ null,
+ pht('Name'),
+ pht('Behavior'),
+ pht('Details'),
+ null,
+ ))
+ ->setColumnClasses(
+ array(
+ null,
+ 'pri',
+ null,
+ 'wide',
+ null,
+ ));
+
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader(pht('Plan Behaviors'));
+
+ return id(new PHUIObjectBoxView())
+ ->setHeader($header)
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->setTable($table);
+ }
+
}
diff --git a/src/applications/harbormaster/controller/HarbormasterUnitMessageListController.php b/src/applications/harbormaster/controller/HarbormasterUnitMessageListController.php
index d548ceac9..a87d17c4f 100644
--- a/src/applications/harbormaster/controller/HarbormasterUnitMessageListController.php
+++ b/src/applications/harbormaster/controller/HarbormasterUnitMessageListController.php
@@ -1,72 +1,73 @@
<?php
final class HarbormasterUnitMessageListController
extends HarbormasterController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$buildable = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->needBuilds(true)
->needTargets(true)
->executeOne();
if (!$buildable) {
return new Aphront404Response();
}
$id = $buildable->getID();
$target_phids = array();
foreach ($buildable->getBuilds() as $build) {
foreach ($build->getBuildTargets() as $target) {
$target_phids[] = $target->getPHID();
}
}
$unit_data = array();
if ($target_phids) {
- $unit_data = id(new HarbormasterBuildUnitMessage())->loadAllWhere(
- 'buildTargetPHID IN (%Ls)',
- $target_phids);
+ $unit_data = id(new HarbormasterBuildUnitMessageQuery())
+ ->setViewer($viewer)
+ ->withBuildTargetPHIDs($target_phids)
+ ->execute();
} else {
$unit_data = array();
}
$unit = id(new HarbormasterUnitSummaryView())
->setViewer($viewer)
->setBuildable($buildable)
->setUnitMessages($unit_data);
$crumbs = $this->buildApplicationCrumbs();
$this->addBuildableCrumb($crumbs, $buildable);
$crumbs->addTextCrumb(pht('Unit Tests'));
$crumbs->setBorder(true);
$title = array(
$buildable->getMonogram(),
pht('Unit Tests'),
);
$header = id(new PHUIHeaderView())
->setHeader($buildable->getMonogram().' '.pht('Unit Tests'));
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(
$unit,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterUnitMessageViewController.php b/src/applications/harbormaster/controller/HarbormasterUnitMessageViewController.php
index 5cb33c0c9..7111db654 100644
--- a/src/applications/harbormaster/controller/HarbormasterUnitMessageViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterUnitMessageViewController.php
@@ -1,116 +1,119 @@
<?php
final class HarbormasterUnitMessageViewController
extends HarbormasterController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$message_id = $request->getURIData('id');
- $message = id(new HarbormasterBuildUnitMessage())->load($message_id);
+ $message = id(new HarbormasterBuildUnitMessageQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($message_id))
+ ->executeOne();
if (!$message) {
return new Aphront404Response();
}
$build_target = id(new HarbormasterBuildTargetQuery())
->setViewer($viewer)
->withPHIDs(array($message->getBuildTargetPHID()))
->executeOne();
if (!$build_target) {
return new Aphront404Response();
}
$build = $build_target->getBuild();
$buildable = $build->getBuildable();
$buildable_id = $buildable->getID();
$id = $message->getID();
$display_name = $message->getUnitMessageDisplayName();
$status = $message->getResult();
$status_icon = HarbormasterUnitStatus::getUnitStatusIcon($status);
$status_color = HarbormasterUnitStatus::getUnitStatusColor($status);
$status_label = HarbormasterUnitStatus::getUnitStatusLabel($status);
$header = id(new PHUIHeaderView())
->setHeader($display_name)
->setStatus($status_icon, $status_color, $status_label);
$properties = $this->buildPropertyListView($message);
$curtain = $this->buildCurtainView($message, $build);
$unit = id(new PHUIObjectBoxView())
->setHeaderText(pht('TEST RESULT'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addPropertyList($properties);
$crumbs = $this->buildApplicationCrumbs();
$this->addBuildableCrumb($crumbs, $buildable);
$crumbs->addTextCrumb(
pht('Unit Tests'),
"/harbormaster/unit/{$buildable_id}/");
$crumbs->addTextCrumb(pht('Unit %d', $id));
$crumbs->setBorder(true);
$title = array(
$display_name,
$buildable->getMonogram(),
);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(array(
$unit,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
private function buildPropertyListView(
HarbormasterBuildUnitMessage $message) {
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setUser($viewer);
$view->addProperty(
pht('Run At'),
phabricator_datetime($message->getDateCreated(), $viewer));
$details = $message->newUnitMessageDetailsView($viewer);
$view->addSectionHeader(
pht('Details'),
PHUIPropertyListView::ICON_TESTPLAN);
$view->addTextContent($details);
return $view;
}
private function buildCurtainView(
HarbormasterBuildUnitMessage $message,
HarbormasterBuild $build) {
$viewer = $this->getViewer();
$curtain = $this->newCurtainView($build);
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('View Build'))
->setHref($build->getURI())
->setIcon('fa-wrench'));
return $curtain;
}
}
diff --git a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php
index 11837051c..c0fa80d71 100644
--- a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php
+++ b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php
@@ -1,93 +1,124 @@
<?php
final class HarbormasterBuildPlanEditEngine
extends PhabricatorEditEngine {
const ENGINECONST = 'harbormaster.buildplan';
public function isEngineConfigurable() {
return false;
}
public function getEngineName() {
return pht('Harbormaster Build Plans');
}
public function getSummaryHeader() {
return pht('Edit Harbormaster Build Plan Configurations');
}
public function getSummaryText() {
return pht('This engine is used to edit Harbormaster build plans.');
}
public function getEngineApplicationClass() {
return 'PhabricatorHarbormasterApplication';
}
protected function newEditableObject() {
$viewer = $this->getViewer();
return HarbormasterBuildPlan::initializeNewBuildPlan($viewer);
}
protected function newObjectQuery() {
return new HarbormasterBuildPlanQuery();
}
protected function getObjectCreateTitleText($object) {
return pht('Create Build Plan');
}
protected function getObjectCreateButtonText($object) {
return pht('Create Build Plan');
}
protected function getObjectEditTitleText($object) {
return pht('Edit Build Plan: %s', $object->getName());
}
protected function getObjectEditShortText($object) {
return pht('Edit Build Plan');
}
protected function getObjectCreateShortText() {
return pht('Create Build Plan');
}
protected function getObjectName() {
return pht('Build Plan');
}
protected function getEditorURI() {
return '/harbormaster/plan/edit/';
}
protected function getObjectCreateCancelURI($object) {
return '/harbormaster/plan/';
}
protected function getObjectViewURI($object) {
$id = $object->getID();
return "/harbormaster/plan/{$id}/";
}
protected function getCreateNewObjectPolicy() {
return $this->getApplication()->getPolicy(
HarbormasterCreatePlansCapability::CAPABILITY);
}
protected function buildCustomEditFields($object) {
- return array(
+ $fields = array(
id(new PhabricatorTextEditField())
->setKey('name')
->setLabel(pht('Name'))
->setIsRequired(true)
- ->setTransactionType(HarbormasterBuildPlanTransaction::TYPE_NAME)
+ ->setTransactionType(
+ HarbormasterBuildPlanNameTransaction::TRANSACTIONTYPE)
->setDescription(pht('The build plan name.'))
->setConduitDescription(pht('Rename the plan.'))
->setConduitTypeDescription(pht('New plan name.'))
->setValue($object->getName()),
);
+
+
+ $metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey();
+
+ $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
+ foreach ($behaviors as $behavior) {
+ $key = $behavior->getKey();
+
+ // Get the raw key off the object so that we don't reset stuff to
+ // default values by mistake if a behavior goes missing somehow.
+ $storage_key = HarbormasterBuildPlanBehavior::getStorageKeyForBehaviorKey(
+ $key);
+ $behavior_option = $object->getPlanProperty($storage_key);
+
+ if (!strlen($behavior_option)) {
+ $behavior_option = $behavior->getPlanOption($object)->getKey();
+ }
+
+ $fields[] = id(new PhabricatorSelectEditField())
+ ->setIsFormField(false)
+ ->setKey(sprintf('behavior.%s', $behavior->getKey()))
+ ->setMetadataValue($metadata_key, $behavior->getKey())
+ ->setLabel(pht('Behavior: %s', $behavior->getName()))
+ ->setTransactionType(
+ HarbormasterBuildPlanBehaviorTransaction::TRANSACTIONTYPE)
+ ->setValue($behavior_option)
+ ->setOptions($behavior->getOptionMap());
+ }
+
+ return $fields;
}
}
diff --git a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditor.php b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditor.php
index 71c9283ad..1b340b652 100644
--- a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditor.php
+++ b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditor.php
@@ -1,110 +1,33 @@
<?php
final class HarbormasterBuildPlanEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorHarbormasterApplication';
}
public function getEditorObjectsDescription() {
return pht('Harbormaster Build Plans');
}
+ public function getCreateObjectTitle($author, $object) {
+ return pht('%s created this build plan.', $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[] = HarbormasterBuildPlanTransaction::TYPE_NAME;
- $types[] = HarbormasterBuildPlanTransaction::TYPE_STATUS;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
- protected function getCustomTransactionOldValue(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
- switch ($xaction->getTransactionType()) {
- case HarbormasterBuildPlanTransaction::TYPE_NAME:
- if ($this->getIsNewObject()) {
- return null;
- }
- return $object->getName();
- case HarbormasterBuildPlanTransaction::TYPE_STATUS:
- return $object->getPlanStatus();
- }
-
- return parent::getCustomTransactionOldValue($object, $xaction);
- }
-
- protected function getCustomTransactionNewValue(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
- switch ($xaction->getTransactionType()) {
- case HarbormasterBuildPlanTransaction::TYPE_NAME:
- return $xaction->getNewValue();
- case HarbormasterBuildPlanTransaction::TYPE_STATUS:
- return $xaction->getNewValue();
- }
- return parent::getCustomTransactionNewValue($object, $xaction);
- }
-
- protected function applyCustomInternalTransaction(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
- switch ($xaction->getTransactionType()) {
- case HarbormasterBuildPlanTransaction::TYPE_NAME:
- $object->setName($xaction->getNewValue());
- return;
- case HarbormasterBuildPlanTransaction::TYPE_STATUS:
- $object->setPlanStatus($xaction->getNewValue());
- return;
- }
- return parent::applyCustomInternalTransaction($object, $xaction);
- }
-
- protected function applyCustomExternalTransaction(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
- switch ($xaction->getTransactionType()) {
- case HarbormasterBuildPlanTransaction::TYPE_NAME:
- case HarbormasterBuildPlanTransaction::TYPE_STATUS:
- return;
- }
- return parent::applyCustomExternalTransaction($object, $xaction);
- }
-
- protected function validateTransaction(
- PhabricatorLiskDAO $object,
- $type,
- array $xactions) {
-
- $errors = parent::validateTransaction($object, $type, $xactions);
-
- switch ($type) {
- case HarbormasterBuildPlanTransaction::TYPE_NAME:
- $missing = $this->validateIsEmptyTextField(
- $object->getName(),
- $xactions);
-
- if ($missing) {
- $error = new PhabricatorApplicationTransactionValidationError(
- $type,
- pht('Required'),
- pht('You must choose a name for your build plan.'),
- last($xactions));
-
- $error->setIsMissingFieldError(true);
- $errors[] = $error;
- }
- break;
- }
-
- return $errors;
- }
-
-
}
diff --git a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php
index 170e4c8a5..447bd5370 100644
--- a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php
+++ b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php
@@ -1,605 +1,629 @@
<?php
/**
* Moves a build forward by queuing build tasks, canceling or restarting the
* build, or failing it in response to task failures.
*/
final class HarbormasterBuildEngine extends Phobject {
private $build;
private $viewer;
private $newBuildTargets = array();
private $artifactReleaseQueue = array();
private $forceBuildableUpdate;
public function setForceBuildableUpdate($force_buildable_update) {
$this->forceBuildableUpdate = $force_buildable_update;
return $this;
}
public function shouldForceBuildableUpdate() {
return $this->forceBuildableUpdate;
}
public function queueNewBuildTarget(HarbormasterBuildTarget $target) {
$this->newBuildTargets[] = $target;
return $this;
}
public function getNewBuildTargets() {
return $this->newBuildTargets;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setBuild(HarbormasterBuild $build) {
$this->build = $build;
return $this;
}
public function getBuild() {
return $this->build;
}
public function continueBuild() {
$build = $this->getBuild();
$lock_key = 'harbormaster.build:'.$build->getID();
$lock = PhabricatorGlobalLock::newLock($lock_key)->lock(15);
$build->reload();
$old_status = $build->getBuildStatus();
try {
$this->updateBuild($build);
} catch (Exception $ex) {
// If any exception is raised, the build is marked as a failure and the
// exception is re-thrown (this ensures we don't leave builds in an
// inconsistent state).
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_ERROR);
$build->save();
$lock->unlock();
$this->releaseAllArtifacts($build);
throw $ex;
}
$lock->unlock();
// NOTE: We queue new targets after releasing the lock so that in-process
// execution via `bin/harbormaster` does not reenter the locked region.
foreach ($this->getNewBuildTargets() as $target) {
$task = PhabricatorWorker::scheduleTask(
'HarbormasterTargetWorker',
array(
'targetID' => $target->getID(),
),
array(
'objectPHID' => $target->getPHID(),
));
}
// If the build changed status, we might need to update the overall status
// on the buildable.
$new_status = $build->getBuildStatus();
if ($new_status != $old_status || $this->shouldForceBuildableUpdate()) {
$this->updateBuildable($build->getBuildable());
}
$this->releaseQueuedArtifacts();
// If we are no longer building for any reason, release all artifacts.
if (!$build->isBuilding()) {
$this->releaseAllArtifacts($build);
}
}
private function updateBuild(HarbormasterBuild $build) {
if ($build->isAborting()) {
$this->releaseAllArtifacts($build);
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_ABORTED);
$build->save();
}
if (($build->getBuildStatus() == HarbormasterBuildStatus::STATUS_PENDING) ||
($build->isRestarting())) {
$this->restartBuild($build);
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING);
$build->save();
}
if ($build->isResuming()) {
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING);
$build->save();
}
if ($build->isPausing() && !$build->isComplete()) {
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_PAUSED);
$build->save();
}
$build->deleteUnprocessedCommands();
if ($build->getBuildStatus() == HarbormasterBuildStatus::STATUS_BUILDING) {
$this->updateBuildSteps($build);
}
}
private function restartBuild(HarbormasterBuild $build) {
// We're restarting the build, so release all previous artifacts.
$this->releaseAllArtifacts($build);
// Increment the build generation counter on the build.
$build->setBuildGeneration($build->getBuildGeneration() + 1);
// Currently running targets should periodically check their build
// generation (which won't have changed) against the build's generation.
// If it is different, they will automatically stop what they're doing
// and abort.
// Previously we used to delete targets, logs and artifacts here. Instead,
// leave them around so users can view previous generations of this build.
}
private function updateBuildSteps(HarbormasterBuild $build) {
$all_targets = id(new HarbormasterBuildTargetQuery())
->setViewer($this->getViewer())
->withBuildPHIDs(array($build->getPHID()))
->withBuildGenerations(array($build->getBuildGeneration()))
->execute();
$this->updateWaitingTargets($all_targets);
$targets = mgroup($all_targets, 'getBuildStepPHID');
$steps = id(new HarbormasterBuildStepQuery())
->setViewer($this->getViewer())
->withBuildPlanPHIDs(array($build->getBuildPlan()->getPHID()))
->execute();
$steps = mpull($steps, null, 'getPHID');
// Identify steps which are in various states.
$queued = array();
$underway = array();
$waiting = array();
$complete = array();
$failed = array();
foreach ($steps as $step) {
$step_targets = idx($targets, $step->getPHID(), array());
if ($step_targets) {
$is_queued = false;
$is_underway = false;
foreach ($step_targets as $target) {
if ($target->isUnderway()) {
$is_underway = true;
break;
}
}
$is_waiting = false;
foreach ($step_targets as $target) {
if ($target->isWaiting()) {
$is_waiting = true;
break;
}
}
$is_complete = true;
foreach ($step_targets as $target) {
if (!$target->isComplete()) {
$is_complete = false;
break;
}
}
$is_failed = false;
foreach ($step_targets as $target) {
if ($target->isFailed()) {
$is_failed = true;
break;
}
}
} else {
$is_queued = true;
$is_underway = false;
$is_waiting = false;
$is_complete = false;
$is_failed = false;
}
if ($is_queued) {
$queued[$step->getPHID()] = true;
}
if ($is_underway) {
$underway[$step->getPHID()] = true;
}
if ($is_waiting) {
$waiting[$step->getPHID()] = true;
}
if ($is_complete) {
$complete[$step->getPHID()] = true;
}
if ($is_failed) {
$failed[$step->getPHID()] = true;
}
}
// If any step failed, fail the whole build, then bail.
if (count($failed)) {
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_FAILED);
$build->save();
return;
}
// If every step is complete, we're done with this build. Mark it passed
// and bail.
if (count($complete) == count($steps)) {
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_PASSED);
$build->save();
return;
}
// Release any artifacts which are not inputs to any remaining build
// step. We're done with these, so something else is free to use them.
$ongoing_phids = array_keys($queued + $waiting + $underway);
$ongoing_steps = array_select_keys($steps, $ongoing_phids);
$this->releaseUnusedArtifacts($all_targets, $ongoing_steps);
// Identify all the steps which are ready to run (because all their
// dependencies are complete).
$runnable = array();
foreach ($steps as $step) {
$dependencies = $step->getStepImplementation()->getDependencies($step);
if (isset($queued[$step->getPHID()])) {
$can_run = true;
foreach ($dependencies as $dependency) {
if (empty($complete[$dependency])) {
$can_run = false;
break;
}
}
if ($can_run) {
$runnable[] = $step;
}
}
}
if (!$runnable && !$waiting && !$underway) {
// This means the build is deadlocked, and the user has configured
// circular dependencies.
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_DEADLOCKED);
$build->save();
return;
}
foreach ($runnable as $runnable_step) {
$target = HarbormasterBuildTarget::initializeNewBuildTarget(
$build,
$runnable_step,
$build->retrieveVariablesFromBuild());
$target->save();
$this->queueNewBuildTarget($target);
}
}
/**
* Release any artifacts which aren't used by any running or waiting steps.
*
* This releases artifacts as soon as they're no longer used. This can be
* particularly relevant when a build uses multiple hosts since it returns
* hosts to the pool more quickly.
*
* @param list<HarbormasterBuildTarget> Targets in the build.
* @param list<HarbormasterBuildStep> List of running and waiting steps.
* @return void
*/
private function releaseUnusedArtifacts(array $targets, array $steps) {
assert_instances_of($targets, 'HarbormasterBuildTarget');
assert_instances_of($steps, 'HarbormasterBuildStep');
if (!$targets || !$steps) {
return;
}
$target_phids = mpull($targets, 'getPHID');
$artifacts = id(new HarbormasterBuildArtifactQuery())
->setViewer($this->getViewer())
->withBuildTargetPHIDs($target_phids)
->withIsReleased(false)
->execute();
if (!$artifacts) {
return;
}
// Collect all the artifacts that remaining build steps accept as inputs.
$must_keep = array();
foreach ($steps as $step) {
$inputs = $step->getStepImplementation()->getArtifactInputs();
foreach ($inputs as $input) {
$artifact_key = $input['key'];
$must_keep[$artifact_key] = true;
}
}
// Queue unreleased artifacts which no remaining step uses for immediate
// release.
foreach ($artifacts as $artifact) {
$key = $artifact->getArtifactKey();
if (isset($must_keep[$key])) {
continue;
}
$this->artifactReleaseQueue[] = $artifact;
}
}
/**
* Process messages which were sent to these targets, kicking applicable
* targets out of "Waiting" and into either "Passed" or "Failed".
*
* @param list<HarbormasterBuildTarget> List of targets to process.
* @return void
*/
private function updateWaitingTargets(array $targets) {
assert_instances_of($targets, 'HarbormasterBuildTarget');
// We only care about messages for targets which are actually in a waiting
// state.
$waiting_targets = array();
foreach ($targets as $target) {
if ($target->isWaiting()) {
$waiting_targets[$target->getPHID()] = $target;
}
}
if (!$waiting_targets) {
return;
}
$messages = id(new HarbormasterBuildMessageQuery())
->setViewer($this->getViewer())
->withReceiverPHIDs(array_keys($waiting_targets))
->withConsumed(false)
->execute();
foreach ($messages as $message) {
$target = $waiting_targets[$message->getReceiverPHID()];
switch ($message->getType()) {
case HarbormasterMessageType::MESSAGE_PASS:
$new_status = HarbormasterBuildTarget::STATUS_PASSED;
break;
case HarbormasterMessageType::MESSAGE_FAIL:
$new_status = HarbormasterBuildTarget::STATUS_FAILED;
break;
case HarbormasterMessageType::MESSAGE_WORK:
default:
$new_status = null;
break;
}
if ($new_status !== null) {
$message->setIsConsumed(true);
$message->save();
$target->setTargetStatus($new_status);
if ($target->isComplete()) {
$target->setDateCompleted(PhabricatorTime::getNow());
}
$target->save();
}
}
}
/**
* Update the overall status of the buildable this build is attached to.
*
* After a build changes state (for example, passes or fails) it may affect
* the overall state of the associated buildable. Compute the new aggregate
* state and save it on the buildable.
*
* @param HarbormasterBuild The buildable to update.
* @return void
*/
public function updateBuildable(HarbormasterBuildable $buildable) {
$viewer = $this->getViewer();
$lock_key = 'harbormaster.buildable:'.$buildable->getID();
$lock = PhabricatorGlobalLock::newLock($lock_key)->lock(15);
$buildable = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withIDs(array($buildable->getID()))
->needBuilds(true)
->executeOne();
$messages = id(new HarbormasterBuildMessageQuery())
->setViewer($viewer)
->withReceiverPHIDs(array($buildable->getPHID()))
->withConsumed(false)
->execute();
$done_preparing = false;
$update_container = false;
foreach ($messages as $message) {
switch ($message->getType()) {
case HarbormasterMessageType::BUILDABLE_BUILD:
$done_preparing = true;
break;
case HarbormasterMessageType::BUILDABLE_CONTAINER:
$update_container = true;
break;
default:
break;
}
$message
->setIsConsumed(true)
->save();
}
// If we received a "build" command, all builds are scheduled and we can
// move out of "preparing" into "building".
if ($done_preparing) {
if ($buildable->isPreparing()) {
$buildable
->setBuildableStatus(HarbormasterBuildableStatus::STATUS_BUILDING)
->save();
}
}
// If we've been informed that the container for the buildable has
// changed, update it.
if ($update_container) {
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($buildable->getBuildablePHID()))
->executeOne();
if ($object) {
$buildable
->setContainerPHID($object->getHarbormasterContainerPHID())
->save();
}
}
$old = clone $buildable;
// Don't update the buildable status if we're still preparing builds: more
// builds may still be scheduled shortly, so even if every build we know
// about so far has passed, that doesn't mean the buildable has actually
// passed everything it needs to.
if (!$buildable->isPreparing()) {
+ $behavior_key = HarbormasterBuildPlanBehavior::BEHAVIOR_BUILDABLE;
+ $behavior = HarbormasterBuildPlanBehavior::getBehavior($behavior_key);
+
+ $key_never = HarbormasterBuildPlanBehavior::BUILDABLE_NEVER;
+ $key_building = HarbormasterBuildPlanBehavior::BUILDABLE_IF_BUILDING;
+
$all_pass = true;
$any_fail = false;
foreach ($buildable->getBuilds() as $build) {
+ $plan = $build->getBuildPlan();
+ $option = $behavior->getPlanOption($plan);
+ $option_key = $option->getKey();
+
+ $is_never = ($option_key === $key_never);
+ $is_building = ($option_key === $key_building);
+
+ // If this build "Never" affects the buildable, ignore it.
+ if ($is_never) {
+ continue;
+ }
+
+ // If this build affects the buildable "If Building", but is already
+ // complete, ignore it.
+ if ($is_building && $build->isComplete()) {
+ continue;
+ }
+
if (!$build->isPassed()) {
$all_pass = false;
}
if ($build->isComplete() && !$build->isPassed()) {
$any_fail = true;
}
}
if ($any_fail) {
$new_status = HarbormasterBuildableStatus::STATUS_FAILED;
} else if ($all_pass) {
$new_status = HarbormasterBuildableStatus::STATUS_PASSED;
} else {
$new_status = HarbormasterBuildableStatus::STATUS_BUILDING;
}
$did_update = ($old->getBuildableStatus() !== $new_status);
if ($did_update) {
$buildable->setBuildableStatus($new_status);
$buildable->save();
}
}
$lock->unlock();
// Don't publish anything if we're still preparing builds.
if ($buildable->isPreparing()) {
return;
}
$this->publishBuildable($old, $buildable);
}
public function publishBuildable(
HarbormasterBuildable $old,
HarbormasterBuildable $new) {
$viewer = $this->getViewer();
// Publish the buildable. We publish buildables even if they haven't
// changed status in Harbormaster because applications may care about
// different things than Harbormaster does. For example, Differential
// does not care about local lint and unit tests when deciding whether
// a revision should move out of draft or not.
// NOTE: We're publishing both automatic and manual buildables. Buildable
// objects should generally ignore manual buildables, but it's up to them
// to decide.
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($new->getBuildablePHID()))
->executeOne();
if (!$object) {
return;
}
$engine = HarbormasterBuildableEngine::newForObject($object, $viewer);
$daemon_source = PhabricatorContentSource::newForSource(
PhabricatorDaemonContentSource::SOURCECONST);
$harbormaster_phid = id(new PhabricatorHarbormasterApplication())
->getPHID();
$engine
->setActingAsPHID($harbormaster_phid)
->setContentSource($daemon_source)
->publishBuildable($old, $new);
}
private function releaseAllArtifacts(HarbormasterBuild $build) {
$targets = id(new HarbormasterBuildTargetQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withBuildPHIDs(array($build->getPHID()))
->withBuildGenerations(array($build->getBuildGeneration()))
->execute();
if (count($targets) === 0) {
return;
}
$target_phids = mpull($targets, 'getPHID');
$artifacts = id(new HarbormasterBuildArtifactQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withBuildTargetPHIDs($target_phids)
->withIsReleased(false)
->execute();
foreach ($artifacts as $artifact) {
$artifact->releaseArtifact();
}
}
private function releaseQueuedArtifacts() {
foreach ($this->artifactReleaseQueue as $key => $artifact) {
$artifact->releaseArtifact();
unset($this->artifactReleaseQueue[$key]);
}
}
}
diff --git a/src/applications/harbormaster/exception/HarbormasterRestartException.php b/src/applications/harbormaster/exception/HarbormasterRestartException.php
new file mode 100644
index 000000000..bd0b86184
--- /dev/null
+++ b/src/applications/harbormaster/exception/HarbormasterRestartException.php
@@ -0,0 +1,33 @@
+<?php
+
+final class HarbormasterRestartException extends Exception {
+
+ private $title;
+ private $body = array();
+
+ public function __construct($title, $body = null) {
+ $this->setTitle($title);
+ $this->appendParagraph($body);
+
+ parent::__construct($title);
+ }
+
+ public function setTitle($title) {
+ $this->title = $title;
+ return $this;
+ }
+
+ public function getTitle() {
+ return $this->title;
+ }
+
+ public function appendParagraph($description) {
+ $this->body[] = $description;
+ return $this;
+ }
+
+ public function getBody() {
+ return $this->body;
+ }
+
+}
diff --git a/src/applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php b/src/applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php
index 8c718e5f5..9fc053e8a 100644
--- a/src/applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php
+++ b/src/applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php
@@ -1,94 +1,99 @@
<?php
final class HarbormasterRunBuildPlansHeraldAction
extends HeraldAction {
const DO_BUILD = 'do.build';
const ACTIONCONST = 'harbormaster.build';
public function getRequiredAdapterStates() {
return array(
HeraldBuildableState::STATECONST,
);
}
public function getActionGroupKey() {
return HeraldSupportActionGroup::ACTIONGROUPKEY;
}
public function supportsObject($object) {
$adapter = $this->getAdapter();
return ($adapter instanceof HarbormasterBuildableAdapterInterface);
}
protected function applyBuilds(array $phids, HeraldRule $rule) {
$adapter = $this->getAdapter();
$allowed_types = array(
HarbormasterBuildPlanPHIDType::TYPECONST,
);
$targets = $this->loadStandardTargets($phids, $allowed_types, array());
if (!$targets) {
return;
}
$phids = array_fuse(array_keys($targets));
foreach ($phids as $phid) {
$request = id(new HarbormasterBuildRequest())
->setBuildPlanPHID($phid)
->setInitiatorPHID($rule->getPHID());
$adapter->queueHarbormasterBuildRequest($request);
}
$this->logEffect(self::DO_BUILD, $phids);
}
protected function getActionEffectMap() {
return array(
self::DO_BUILD => array(
'icon' => 'fa-play',
'color' => 'green',
'name' => pht('Building'),
),
);
}
protected function renderActionEffectDescription($type, $data) {
switch ($type) {
case self::DO_BUILD:
return pht(
'Started %s build(s): %s.',
phutil_count($data),
$this->renderHandleList($data));
}
}
public function getHeraldActionName() {
return pht('Run build plans');
}
public function supportsRuleType($rule_type) {
return ($rule_type != HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
}
public function applyEffect($object, HeraldEffect $effect) {
return $this->applyBuilds($effect->getTarget(), $effect->getRule());
}
public function getHeraldActionStandardType() {
return self::STANDARD_PHID_LIST;
}
protected function getDatasource() {
return new HarbormasterBuildPlanDatasource();
}
public function renderActionDescription($value) {
return pht(
'Run build plans: %s.',
$this->renderHandleList($value));
}
+
+ public function getPHIDsAffectedByAction(HeraldActionRecord $record) {
+ return $record->getTarget();
+ }
+
}
diff --git a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php
new file mode 100644
index 000000000..112926c47
--- /dev/null
+++ b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php
@@ -0,0 +1,398 @@
+<?php
+
+final class HarbormasterBuildPlanBehavior
+ extends Phobject {
+
+ private $key;
+ private $name;
+ private $options;
+ private $defaultKey;
+ private $editInstructions;
+
+ const BEHAVIOR_RUNNABLE = 'runnable';
+ const RUNNABLE_IF_VIEWABLE = 'view';
+ const RUNNABLE_IF_EDITABLE = 'edit';
+
+ const BEHAVIOR_RESTARTABLE = 'restartable';
+ const RESTARTABLE_ALWAYS = 'always';
+ const RESTARTABLE_IF_FAILED = 'failed';
+ const RESTARTABLE_NEVER = 'never';
+
+ const BEHAVIOR_DRAFTS = 'hold-drafts';
+ const DRAFTS_ALWAYS = 'always';
+ const DRAFTS_IF_BUILDING = 'building';
+ const DRAFTS_NEVER = 'never';
+
+ const BEHAVIOR_BUILDABLE = 'buildable';
+ const BUILDABLE_ALWAYS = 'always';
+ const BUILDABLE_IF_BUILDING = 'building';
+ const BUILDABLE_NEVER = 'never';
+
+ const BEHAVIOR_LANDWARNING = 'arc-land';
+ const LANDWARNING_ALWAYS = 'always';
+ const LANDWARNING_IF_BUILDING = 'building';
+ const LANDWARNING_IF_COMPLETE = 'complete';
+ const LANDWARNING_NEVER = 'never';
+
+ public function setKey($key) {
+ $this->key = $key;
+ return $this;
+ }
+
+ public function getKey() {
+ return $this->key;
+ }
+
+ public function setName($name) {
+ $this->name = $name;
+ return $this;
+ }
+
+ public function getName() {
+ return $this->name;
+ }
+
+ public function setEditInstructions($edit_instructions) {
+ $this->editInstructions = $edit_instructions;
+ return $this;
+ }
+
+ public function getEditInstructions() {
+ return $this->editInstructions;
+ }
+
+ public function getOptionMap() {
+ return mpull($this->options, 'getName', 'getKey');
+ }
+
+ public function setOptions(array $options) {
+ assert_instances_of($options, 'HarbormasterBuildPlanBehaviorOption');
+
+ $key_map = array();
+ $default = null;
+
+ foreach ($options as $option) {
+ $key = $option->getKey();
+
+ if (isset($key_map[$key])) {
+ throw new Exception(
+ pht(
+ 'Multiple behavior options (for behavior "%s") have the same '.
+ 'key ("%s"). Each option must have a unique key.',
+ $this->getKey(),
+ $key));
+ }
+ $key_map[$key] = true;
+
+ if ($option->getIsDefault()) {
+ if ($default === null) {
+ $default = $key;
+ } else {
+ throw new Exception(
+ pht(
+ 'Multiple behavior options (for behavior "%s") are marked as '.
+ 'default options ("%s" and "%s"). Exactly one option must be '.
+ 'marked as the default option.',
+ $this->getKey(),
+ $default,
+ $key));
+ }
+ }
+ }
+
+ if ($default === null) {
+ throw new Exception(
+ pht(
+ 'No behavior option is marked as the default option (for '.
+ 'behavior "%s"). Exactly one option must be marked as the '.
+ 'default option.',
+ $this->getKey()));
+ }
+
+ $this->options = mpull($options, null, 'getKey');
+ $this->defaultKey = $default;
+
+ return $this;
+ }
+
+ public function getOptions() {
+ return $this->options;
+ }
+
+ public function getPlanOption(HarbormasterBuildPlan $plan) {
+ $behavior_key = $this->getKey();
+ $storage_key = self::getStorageKeyForBehaviorKey($behavior_key);
+
+ $plan_value = $plan->getPlanProperty($storage_key);
+ if (isset($this->options[$plan_value])) {
+ return $this->options[$plan_value];
+ }
+
+ return idx($this->options, $this->defaultKey);
+ }
+
+ public static function getTransactionMetadataKey() {
+ return 'behavior-key';
+ }
+
+ public static function getStorageKeyForBehaviorKey($behavior_key) {
+ return sprintf('behavior.%s', $behavior_key);
+ }
+
+ public static function getBehavior($key) {
+ $behaviors = self::newPlanBehaviors();
+
+ if (!isset($behaviors[$key])) {
+ throw new Exception(
+ pht(
+ 'No build plan behavior with key "%s" exists.',
+ $key));
+ }
+
+ return $behaviors[$key];
+ }
+
+ public static function newPlanBehaviors() {
+ $draft_options = array(
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey(self::DRAFTS_ALWAYS)
+ ->setIcon('fa-check-circle-o green')
+ ->setName(pht('Always'))
+ ->setIsDefault(true)
+ ->setDescription(
+ pht(
+ 'Revisions are not sent for review until the build completes, '.
+ 'and are returned to the author for updates if the build fails.')),
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey(self::DRAFTS_IF_BUILDING)
+ ->setIcon('fa-pause-circle-o yellow')
+ ->setName(pht('If Building'))
+ ->setDescription(
+ pht(
+ 'Revisions are not sent for review until the build completes, '.
+ 'but they will be sent for review even if it fails.')),
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey(self::DRAFTS_NEVER)
+ ->setIcon('fa-circle-o red')
+ ->setName(pht('Never'))
+ ->setDescription(
+ pht(
+ 'Revisions are sent for review regardless of the status of the '.
+ 'build.')),
+ );
+
+ $land_options = array(
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey(self::LANDWARNING_ALWAYS)
+ ->setIcon('fa-check-circle-o green')
+ ->setName(pht('Always'))
+ ->setIsDefault(true)
+ ->setDescription(
+ pht(
+ '"arc land" warns if the build is still running or has '.
+ 'failed.')),
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey(self::LANDWARNING_IF_BUILDING)
+ ->setIcon('fa-pause-circle-o yellow')
+ ->setName(pht('If Building'))
+ ->setDescription(
+ pht(
+ '"arc land" warns if the build is still running, but ignores '.
+ 'the build if it has failed.')),
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey(self::LANDWARNING_IF_COMPLETE)
+ ->setIcon('fa-dot-circle-o yellow')
+ ->setName(pht('If Complete'))
+ ->setDescription(
+ pht(
+ '"arc land" warns if the build has failed, but ignores the '.
+ 'build if it is still running.')),
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey(self::LANDWARNING_NEVER)
+ ->setIcon('fa-circle-o red')
+ ->setName(pht('Never'))
+ ->setDescription(
+ pht(
+ '"arc land" never warns that the build is still running or '.
+ 'has failed.')),
+ );
+
+ $aggregate_options = array(
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey(self::BUILDABLE_ALWAYS)
+ ->setIcon('fa-check-circle-o green')
+ ->setName(pht('Always'))
+ ->setIsDefault(true)
+ ->setDescription(
+ pht(
+ 'The buildable waits for the build, and fails if the '.
+ 'build fails.')),
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey(self::BUILDABLE_IF_BUILDING)
+ ->setIcon('fa-pause-circle-o yellow')
+ ->setName(pht('If Building'))
+ ->setDescription(
+ pht(
+ 'The buildable waits for the build, but does not fail '.
+ 'if the build fails.')),
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey(self::BUILDABLE_NEVER)
+ ->setIcon('fa-circle-o red')
+ ->setName(pht('Never'))
+ ->setDescription(
+ pht(
+ 'The buildable does not wait for the build.')),
+ );
+
+ $restart_options = array(
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey(self::RESTARTABLE_ALWAYS)
+ ->setIcon('fa-repeat green')
+ ->setName(pht('Always'))
+ ->setIsDefault(true)
+ ->setDescription(
+ pht('The build may be restarted.')),
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey(self::RESTARTABLE_IF_FAILED)
+ ->setIcon('fa-times-circle-o yellow')
+ ->setName(pht('If Failed'))
+ ->setDescription(
+ pht('The build may be restarted if it has failed.')),
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey(self::RESTARTABLE_NEVER)
+ ->setIcon('fa-times red')
+ ->setName(pht('Never'))
+ ->setDescription(
+ pht('The build may not be restarted.')),
+ );
+
+ $run_options = array(
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey(self::RUNNABLE_IF_EDITABLE)
+ ->setIcon('fa-pencil green')
+ ->setName(pht('If Editable'))
+ ->setIsDefault(true)
+ ->setDescription(
+ pht('Only users who can edit the plan can run it manually.')),
+ id(new HarbormasterBuildPlanBehaviorOption())
+ ->setKey(self::RUNNABLE_IF_VIEWABLE)
+ ->setIcon('fa-exclamation-triangle yellow')
+ ->setName(pht('If Viewable'))
+ ->setDescription(
+ pht(
+ 'Any user who can view the plan can run it manually.')),
+ );
+
+ $behaviors = array(
+ id(new self())
+ ->setKey(self::BEHAVIOR_DRAFTS)
+ ->setName(pht('Hold Drafts'))
+ ->setEditInstructions(
+ pht(
+ 'When users create revisions in Differential, the default '.
+ 'behavior is to hold them in the "Draft" state until all builds '.
+ 'pass. Once builds pass, the revisions promote and are sent for '.
+ 'review, which notifies reviewers.'.
+ "\n\n".
+ 'The general intent of this workflow is to make sure reviewers '.
+ 'are only spending time on review once changes survive automated '.
+ 'tests. If a change does not pass tests, it usually is not '.
+ 'really ready for review.'.
+ "\n\n".
+ 'If you want to promote revisions out of "Draft" before builds '.
+ 'pass, or promote revisions even when builds fail, you can '.
+ 'change the promotion behavior. This may be useful if you have '.
+ 'very long-running builds, or some builds which are not very '.
+ 'important.'.
+ "\n\n".
+ 'Users may always use "Request Review" to promote a "Draft" '.
+ 'revision, even if builds have failed or are still in progress.'))
+ ->setOptions($draft_options),
+ id(new self())
+ ->setKey(self::BEHAVIOR_LANDWARNING)
+ ->setName(pht('Warn When Landing'))
+ ->setEditInstructions(
+ pht(
+ 'When a user attempts to `arc land` a revision and that revision '.
+ 'has ongoing or failed builds, the default behavior of `arc` is '.
+ 'to warn them about those builds and give them a chance to '.
+ 'reconsider: they may want to wait for ongoing builds to '.
+ 'complete, or fix failed builds before landing the change.'.
+ "\n\n".
+ 'If you do not want to warn users about this build, you can '.
+ 'change the warning behavior. This may be useful if the build '.
+ 'takes a long time to run (so you do not expect users to wait '.
+ 'for it) or the outcome is not important.'.
+ "\n\n".
+ 'This warning is only advisory. Users may always elect to ignore '.
+ 'this warning and continue, even if builds have failed.'.
+ "\n\n".
+ 'This setting also affects the warning that is published to '.
+ 'revisions when commits land with ongoing or failed builds.'))
+ ->setOptions($land_options),
+ id(new self())
+ ->setKey(self::BEHAVIOR_BUILDABLE)
+ ->setEditInstructions(
+ pht(
+ 'The overall state of a buildable (like a commit or revision) is '.
+ 'normally the aggregation of the individual states of all builds '.
+ 'that have run against it.'.
+ "\n\n".
+ 'Buildables are "building" until all builds pass (which changes '.
+ 'them to "pass"), or any build fails (which changes them to '.
+ '"fail").'.
+ "\n\n".
+ 'You can change this behavior if you do not want to wait for this '.
+ 'build, or do not care if it fails.'))
+ ->setName(pht('Affects Buildable'))
+ ->setOptions($aggregate_options),
+ id(new self())
+ ->setKey(self::BEHAVIOR_RESTARTABLE)
+ ->setEditInstructions(
+ pht(
+ 'Usually, builds may be restarted by users who have permission '.
+ 'to edit the related build plan. (You can change who is allowed '.
+ 'to restart a build by adjusting the "Runnable" behavior.)'.
+ "\n\n".
+ 'Restarting a build may be useful if you suspect it has failed '.
+ 'for environmental or circumstantial reasons unrelated to the '.
+ 'actual code, and want to give it another chance at glory.'.
+ "\n\n".
+ 'If you want to prevent a build from being restarted, you can '.
+ 'change when it may be restarted by adjusting this behavior. '.
+ 'This may be useful to prevent accidents where a build with a '.
+ 'dangerous side effect (like deployment) is restarted '.
+ 'improperly.'))
+ ->setName(pht('Restartable'))
+ ->setOptions($restart_options),
+ id(new self())
+ ->setKey(self::BEHAVIOR_RUNNABLE)
+ ->setEditInstructions(
+ pht(
+ 'To run a build manually, you normally must have permission to '.
+ 'edit the related build plan. If you would prefer that anyone who '.
+ 'can see the build plan be able to run and restart the build, you '.
+ 'can change the behavior here.'.
+ "\n\n".
+ 'Note that this controls access to all build management actions: '.
+ '"Run Plan Manually", "Restart", "Abort", "Pause", and "Resume".'.
+ "\n\n".
+ 'WARNING: This may be unsafe, particularly if the build has '.
+ 'side effects like deployment.'.
+ "\n\n".
+ 'If you weaken this policy, an attacker with control of an '.
+ 'account that has "Can View" permission but not "Can Edit" '.
+ 'permission can manually run this build against any old version '.
+ 'of the code, including versions with known security issues.'.
+ "\n\n".
+ 'If running the build has a side effect like deploying code, '.
+ 'they can force deployment of a vulnerable version and then '.
+ 'escalate into an attack against the deployed service.'))
+ ->setName(pht('Runnable'))
+ ->setOptions($run_options),
+ );
+
+ return mpull($behaviors, null, 'getKey');
+ }
+
+}
diff --git a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehaviorOption.php b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehaviorOption.php
new file mode 100644
index 000000000..65b9662b9
--- /dev/null
+++ b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehaviorOption.php
@@ -0,0 +1,57 @@
+<?php
+
+final class HarbormasterBuildPlanBehaviorOption
+ extends Phobject {
+
+ private $name;
+ private $key;
+ private $icon;
+ private $description;
+ private $isDefault;
+
+ public function setName($name) {
+ $this->name = $name;
+ return $this;
+ }
+
+ public function getName() {
+ return $this->name;
+ }
+
+ public function setKey($key) {
+ $this->key = $key;
+ return $this;
+ }
+
+ public function getKey() {
+ return $this->key;
+ }
+
+ public function setDescription($description) {
+ $this->description = $description;
+ return $this;
+ }
+
+ public function getDescription() {
+ return $this->description;
+ }
+
+ public function setIsDefault($is_default) {
+ $this->isDefault = $is_default;
+ return $this;
+ }
+
+ public function getIsDefault() {
+ return $this->isDefault;
+ }
+
+ public function setIcon($icon) {
+ $this->icon = $icon;
+ return $this;
+ }
+
+ public function getIcon() {
+ return $this->icon;
+ }
+
+}
diff --git a/src/applications/harbormaster/query/HarbormasterBuildPlanQuery.php b/src/applications/harbormaster/query/HarbormasterBuildPlanQuery.php
index 405832514..c903fbb37 100644
--- a/src/applications/harbormaster/query/HarbormasterBuildPlanQuery.php
+++ b/src/applications/harbormaster/query/HarbormasterBuildPlanQuery.php
@@ -1,153 +1,152 @@
<?php
final class HarbormasterBuildPlanQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $statuses;
private $datasourceQuery;
private $planAutoKeys;
private $needBuildSteps;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withDatasourceQuery($query) {
$this->datasourceQuery = $query;
return $this;
}
public function withPlanAutoKeys(array $keys) {
$this->planAutoKeys = $keys;
return $this;
}
public function withNameNgrams($ngrams) {
return $this->withNgramsConstraint(
new HarbormasterBuildPlanNameNgrams(),
$ngrams);
}
public function needBuildSteps($need) {
$this->needBuildSteps = $need;
return $this;
}
public function newResultObject() {
return new HarbormasterBuildPlan();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function didFilterPage(array $page) {
if ($this->needBuildSteps) {
$plan_phids = mpull($page, 'getPHID');
$steps = id(new HarbormasterBuildStepQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withBuildPlanPHIDs($plan_phids)
->execute();
$steps = mgroup($steps, 'getBuildPlanPHID');
foreach ($page as $plan) {
$plan_steps = idx($steps, $plan->getPHID(), array());
$plan->attachBuildSteps($plan_steps);
}
}
return $page;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'plan.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'plan.phid IN (%Ls)',
$this->phids);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'plan.planStatus IN (%Ls)',
$this->statuses);
}
if (strlen($this->datasourceQuery)) {
$where[] = qsprintf(
$conn,
'plan.name LIKE %>',
$this->datasourceQuery);
}
if ($this->planAutoKeys !== null) {
$where[] = qsprintf(
$conn,
'plan.planAutoKey IN (%Ls)',
$this->planAutoKeys);
}
return $where;
}
protected function getPrimaryTableAlias() {
return 'plan';
}
public function getQueryApplicationClass() {
return 'PhabricatorHarbormasterApplication';
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'column' => 'name',
'type' => 'string',
'reverse' => true,
),
);
}
- protected function getPagingValueMap($cursor, array $keys) {
- $plan = $this->loadCursorObject($cursor);
+ protected function newPagingMapFromPartialObject($object) {
return array(
- 'id' => $plan->getID(),
- 'name' => $plan->getName(),
+ 'id' => (int)$object->getID(),
+ 'name' => $object->getName(),
);
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name', 'id'),
'name' => pht('Name'),
),
) + parent::getBuiltinOrders();
}
}
diff --git a/src/applications/harbormaster/query/HarbormasterBuildSearchEngine.php b/src/applications/harbormaster/query/HarbormasterBuildSearchEngine.php
index 4cf6a8370..b8140d84f 100644
--- a/src/applications/harbormaster/query/HarbormasterBuildSearchEngine.php
+++ b/src/applications/harbormaster/query/HarbormasterBuildSearchEngine.php
@@ -1,176 +1,141 @@
<?php
final class HarbormasterBuildSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Harbormaster Builds');
}
public function getApplicationClassName() {
return 'PhabricatorHarbormasterApplication';
}
public function newQuery() {
return new HarbormasterBuildQuery();
}
protected function buildCustomSearchFields() {
return array(
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Build Plans'))
->setKey('plans')
->setAliases(array('plan'))
->setDescription(
pht('Search for builds running a given build plan.'))
->setDatasource(new HarbormasterBuildPlanDatasource()),
id(new PhabricatorPHIDsSearchField())
->setLabel(pht('Buildables'))
->setKey('buildables')
->setAliases(array('buildable'))
->setDescription(
pht('Search for builds running against particular buildables.')),
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Statuses'))
->setKey('statuses')
->setAliases(array('status'))
->setDescription(
pht('Search for builds with given statuses.'))
->setDatasource(new HarbormasterBuildStatusDatasource()),
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Initiators'))
->setKey('initiators')
->setAliases(array('initiator'))
->setDescription(
pht(
'Search for builds started by someone or something in particular.'))
->setDatasource(new HarbormasterBuildInitiatorDatasource()),
);
}
protected function getHiddenFields() {
return array(
'buildables',
);
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
if ($map['plans']) {
$query->withBuildPlanPHIDs($map['plans']);
}
if ($map['buildables']) {
$query->withBuildablePHIDs($map['buildables']);
}
if ($map['statuses']) {
$query->withBuildStatuses($map['statuses']);
}
if ($map['initiators']) {
$query->withInitiatorPHIDs($map['initiators']);
}
return $query;
}
protected function getURI($path) {
return '/harbormaster/build/'.$path;
}
protected function getBuiltinQueryNames() {
return array(
'initiated' => pht('My Builds'),
'all' => pht('All Builds'),
'waiting' => pht('Waiting'),
'active' => pht('Active'),
'completed' => pht('Completed'),
);
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'initiated':
$viewer = $this->requireViewer();
return $query->setParameter('initiators', array($viewer->getPHID()));
case 'all':
return $query;
case 'waiting':
return $query
->setParameter(
'statuses',
HarbormasterBuildStatus::getWaitingStatusConstants());
case 'active':
return $query
->setParameter(
'statuses',
HarbormasterBuildStatus::getActiveStatusConstants());
case 'completed':
return $query
->setParameter(
'statuses',
HarbormasterBuildStatus::getCompletedStatusConstants());
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function renderResultList(
array $builds,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($builds, 'HarbormasterBuild');
$viewer = $this->requireViewer();
- $buildables = mpull($builds, 'getBuildable');
- $object_phids = mpull($buildables, 'getBuildablePHID');
- $initiator_phids = mpull($builds, 'getInitiatorPHID');
- $phids = array_mergev(array($initiator_phids, $object_phids));
- $phids = array_unique(array_filter($phids));
-
- $handles = $viewer->loadHandles($phids);
-
- $list = new PHUIObjectItemListView();
- foreach ($builds as $build) {
- $id = $build->getID();
- $initiator = $handles[$build->getInitiatorPHID()];
- $buildable_object = $handles[$build->getBuildable()->getBuildablePHID()];
-
- $item = id(new PHUIObjectItemView())
- ->setViewer($viewer)
- ->setObject($build)
- ->setObjectName(pht('Build %d', $build->getID()))
- ->setHeader($build->getName())
- ->setHref($build->getURI())
- ->setEpoch($build->getDateCreated())
- ->addAttribute($buildable_object->getName());
-
- if ($initiator) {
- $item->addHandleIcon($initiator, $initiator->getName());
- }
-
- $status = $build->getBuildStatus();
-
- $status_icon = HarbormasterBuildStatus::getBuildStatusIcon($status);
- $status_color = HarbormasterBuildStatus::getBuildStatusColor($status);
- $status_label = HarbormasterBuildStatus::getBuildStatusName($status);
-
- $item->setStatusIcon("{$status_icon} {$status_color}", $status_label);
-
- $list->addItem($item);
- }
-
- $result = new PhabricatorApplicationSearchResultView();
- $result->setObjectList($list);
- $result->setNoDataString(pht('No builds found.'));
-
- return $result;
+ $list = id(new HarbormasterBuildView())
+ ->setViewer($viewer)
+ ->setBuilds($builds)
+ ->newObjectList();
+
+ return id(new PhabricatorApplicationSearchResultView())
+ ->setObjectList($list)
+ ->setNoDataString(pht('No builds found.'));
}
}
diff --git a/src/applications/harbormaster/query/HarbormasterBuildUnitMessageQuery.php b/src/applications/harbormaster/query/HarbormasterBuildUnitMessageQuery.php
new file mode 100644
index 000000000..f73016a29
--- /dev/null
+++ b/src/applications/harbormaster/query/HarbormasterBuildUnitMessageQuery.php
@@ -0,0 +1,95 @@
+<?php
+
+final class HarbormasterBuildUnitMessageQuery
+ extends PhabricatorCursorPagedPolicyAwareQuery {
+
+ private $ids;
+ private $phids;
+ private $targetPHIDs;
+
+ public function withIDs(array $ids) {
+ $this->ids = $ids;
+ return $this;
+ }
+
+ public function withPHIDs(array $phids) {
+ $this->phids = $phids;
+ return $this;
+ }
+
+ public function withBuildTargetPHIDs(array $target_phids) {
+ $this->targetPHIDs = $target_phids;
+ return $this;
+ }
+
+ public function newResultObject() {
+ return new HarbormasterBuildUnitMessage();
+ }
+
+ protected function loadPage() {
+ return $this->loadStandardPage($this->newResultObject());
+ }
+
+ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+ $where = parent::buildWhereClauseParts($conn);
+
+ if ($this->ids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'id IN (%Ld)',
+ $this->ids);
+ }
+
+ if ($this->phids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'phid in (%Ls)',
+ $this->phids);
+ }
+
+ if ($this->targetPHIDs !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'buildTargetPHID in (%Ls)',
+ $this->targetPHIDs);
+ }
+
+ return $where;
+ }
+
+ protected function didFilterPage(array $messages) {
+ $indexes = array();
+ foreach ($messages as $message) {
+ $index = $message->getNameIndex();
+ if (strlen($index)) {
+ $indexes[$index] = $index;
+ }
+ }
+
+ if ($indexes) {
+ $map = HarbormasterString::newIndexMap($indexes);
+
+ foreach ($messages as $message) {
+ $index = $message->getNameIndex();
+
+ if (!strlen($index)) {
+ continue;
+ }
+
+ $name = idx($map, $index);
+ if ($name === null) {
+ $name = pht('Unknown Unit Message ("%s")', $index);
+ }
+
+ $message->setName($name);
+ }
+ }
+
+ return $messages;
+ }
+
+ public function getQueryApplicationClass() {
+ return 'PhabricatorHarbormasterApplication';
+ }
+
+}
diff --git a/src/applications/harbormaster/storage/HarbormasterString.php b/src/applications/harbormaster/storage/HarbormasterString.php
new file mode 100644
index 000000000..7493e60e2
--- /dev/null
+++ b/src/applications/harbormaster/storage/HarbormasterString.php
@@ -0,0 +1,54 @@
+<?php
+
+final class HarbormasterString
+ extends HarbormasterDAO {
+
+ protected $stringIndex;
+ protected $stringValue;
+
+ protected function getConfiguration() {
+ return array(
+ self::CONFIG_TIMESTAMPS => false,
+ self::CONFIG_COLUMN_SCHEMA => array(
+ 'stringIndex' => 'bytes12',
+ 'stringValue' => 'text',
+ ),
+ self::CONFIG_KEY_SCHEMA => array(
+ 'key_string' => array(
+ 'columns' => array('stringIndex'),
+ 'unique' => true,
+ ),
+ ),
+ ) + parent::getConfiguration();
+ }
+
+ public static function newIndex($string) {
+ $index = PhabricatorHash::digestForIndex($string);
+
+ $table = new self();
+ $conn = $table->establishConnection('w');
+
+ queryfx(
+ $conn,
+ 'INSERT IGNORE INTO %R (stringIndex, stringValue) VALUES (%s, %s)',
+ $table,
+ $index,
+ $string);
+
+ return $index;
+ }
+
+ public static function newIndexMap(array $indexes) {
+ $table = new self();
+ $conn = $table->establishConnection('r');
+
+ $rows = queryfx_all(
+ $conn,
+ 'SELECT stringIndex, stringValue FROM %R WHERE stringIndex IN (%Ls)',
+ $table,
+ $indexes);
+
+ return ipull($rows, 'stringValue', 'stringIndex');
+ }
+
+}
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php
index 602e38847..70c26827e 100644
--- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php
@@ -1,506 +1,566 @@
<?php
final class HarbormasterBuild extends HarbormasterDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorConduitResultInterface,
PhabricatorDestructibleInterface {
protected $buildablePHID;
protected $buildPlanPHID;
protected $buildStatus;
protected $buildGeneration;
protected $buildParameters = array();
protected $initiatorPHID;
protected $planAutoKey;
private $buildable = self::ATTACHABLE;
private $buildPlan = self::ATTACHABLE;
private $buildTargets = self::ATTACHABLE;
private $unprocessedCommands = self::ATTACHABLE;
public static function initializeNewBuild(PhabricatorUser $actor) {
return id(new HarbormasterBuild())
->setBuildStatus(HarbormasterBuildStatus::STATUS_INACTIVE)
->setBuildGeneration(0);
}
public function delete() {
$this->openTransaction();
$this->deleteUnprocessedCommands();
$result = parent::delete();
$this->saveTransaction();
return $result;
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'buildParameters' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'buildStatus' => 'text32',
'buildGeneration' => 'uint32',
'planAutoKey' => 'text32?',
'initiatorPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_buildable' => array(
'columns' => array('buildablePHID'),
),
'key_plan' => array(
'columns' => array('buildPlanPHID'),
),
'key_status' => array(
'columns' => array('buildStatus'),
),
'key_planautokey' => array(
'columns' => array('buildablePHID', 'planAutoKey'),
'unique' => true,
),
'key_initiator' => array(
'columns' => array('initiatorPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
HarbormasterBuildPHIDType::TYPECONST);
}
public function attachBuildable(HarbormasterBuildable $buildable) {
$this->buildable = $buildable;
return $this;
}
public function getBuildable() {
return $this->assertAttached($this->buildable);
}
public function getName() {
if ($this->getBuildPlan()) {
return $this->getBuildPlan()->getName();
}
return pht('Build');
}
public function attachBuildPlan(
HarbormasterBuildPlan $build_plan = null) {
$this->buildPlan = $build_plan;
return $this;
}
public function getBuildPlan() {
return $this->assertAttached($this->buildPlan);
}
public function getBuildTargets() {
return $this->assertAttached($this->buildTargets);
}
public function attachBuildTargets(array $targets) {
$this->buildTargets = $targets;
return $this;
}
public function isBuilding() {
return $this->getBuildStatusObject()->isBuilding();
}
public function isAutobuild() {
return ($this->getPlanAutoKey() !== null);
}
public function retrieveVariablesFromBuild() {
$results = array(
'buildable.diff' => null,
'buildable.revision' => null,
'buildable.commit' => null,
'repository.callsign' => null,
'repository.phid' => null,
'repository.vcs' => null,
'repository.uri' => null,
'step.timestamp' => null,
'build.id' => null,
'initiator.phid' => null,
);
foreach ($this->getBuildParameters() as $key => $value) {
$results['build/'.$key] = $value;
}
$buildable = $this->getBuildable();
$object = $buildable->getBuildableObject();
$object_variables = $object->getBuildVariables();
$results = $object_variables + $results;
$results['step.timestamp'] = time();
$results['build.id'] = $this->getID();
$results['initiator.phid'] = $this->getInitiatorPHID();
return $results;
}
public static function getAvailableBuildVariables() {
$objects = id(new PhutilClassMapQuery())
->setAncestorClass('HarbormasterBuildableInterface')
->execute();
$variables = array();
$variables[] = array(
'step.timestamp' => pht('The current UNIX timestamp.'),
'build.id' => pht('The ID of the current build.'),
'target.phid' => pht('The PHID of the current build target.'),
'initiator.phid' => pht(
'The PHID of the user or Object that initiated the build, '.
'if applicable.'),
);
foreach ($objects as $object) {
$variables[] = $object->getAvailableBuildVariables();
}
$variables = array_mergev($variables);
return $variables;
}
public function isComplete() {
return $this->getBuildStatusObject()->isComplete();
}
public function isPaused() {
return $this->getBuildStatusObject()->isPaused();
}
public function isPassed() {
return $this->getBuildStatusObject()->isPassed();
}
+ public function isFailed() {
+ return $this->getBuildStatusObject()->isFailed();
+ }
+
public function getURI() {
$id = $this->getID();
return "/harbormaster/build/{$id}/";
}
protected function getBuildStatusObject() {
$status_key = $this->getBuildStatus();
return HarbormasterBuildStatus::newBuildStatusObject($status_key);
}
+ public function getObjectName() {
+ return pht('Build %d', $this->getID());
+ }
+
/* -( Build Commands )----------------------------------------------------- */
private function getUnprocessedCommands() {
return $this->assertAttached($this->unprocessedCommands);
}
public function attachUnprocessedCommands(array $commands) {
$this->unprocessedCommands = $commands;
return $this;
}
public function canRestartBuild() {
- if ($this->isAutobuild()) {
+ try {
+ $this->assertCanRestartBuild();
+ return true;
+ } catch (HarbormasterRestartException $ex) {
return false;
}
+ }
+
+ public function assertCanRestartBuild() {
+ if ($this->isAutobuild()) {
+ throw new HarbormasterRestartException(
+ pht('Can Not Restart Autobuild'),
+ pht(
+ 'This build can not be restarted because it is an automatic '.
+ 'build.'));
+ }
+
+ $restartable = HarbormasterBuildPlanBehavior::BEHAVIOR_RESTARTABLE;
+ $plan = $this->getBuildPlan();
+
+ $option = HarbormasterBuildPlanBehavior::getBehavior($restartable)
+ ->getPlanOption($plan);
+ $option_key = $option->getKey();
+
+ $never_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_NEVER;
+ $is_never = ($option_key === $never_restartable);
+ if ($is_never) {
+ throw new HarbormasterRestartException(
+ pht('Build Plan Prevents Restart'),
+ pht(
+ 'This build can not be restarted because the build plan is '.
+ 'configured to prevent the build from restarting.'));
+ }
+
+ $failed_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_IF_FAILED;
+ $is_failed = ($option_key === $failed_restartable);
+ if ($is_failed) {
+ if (!$this->isFailed()) {
+ throw new HarbormasterRestartException(
+ pht('Only Restartable if Failed'),
+ pht(
+ 'This build can not be restarted because the build plan is '.
+ 'configured to prevent the build from restarting unless it '.
+ 'has failed, and it has not failed.'));
+ }
+ }
- return !$this->isRestarting();
+ if ($this->isRestarting()) {
+ throw new HarbormasterRestartException(
+ pht('Already Restarting'),
+ pht(
+ 'This build is already restarting. You can not reissue a restart '.
+ 'command to a restarting build.'));
+ }
}
public function canPauseBuild() {
if ($this->isAutobuild()) {
return false;
}
return !$this->isComplete() &&
!$this->isPaused() &&
!$this->isPausing();
}
public function canAbortBuild() {
if ($this->isAutobuild()) {
return false;
}
return !$this->isComplete();
}
public function canResumeBuild() {
if ($this->isAutobuild()) {
return false;
}
return $this->isPaused() &&
!$this->isResuming();
}
public function isPausing() {
$is_pausing = false;
foreach ($this->getUnprocessedCommands() as $command_object) {
$command = $command_object->getCommand();
switch ($command) {
case HarbormasterBuildCommand::COMMAND_PAUSE:
$is_pausing = true;
break;
case HarbormasterBuildCommand::COMMAND_RESUME:
case HarbormasterBuildCommand::COMMAND_RESTART:
$is_pausing = false;
break;
case HarbormasterBuildCommand::COMMAND_ABORT:
$is_pausing = true;
break;
}
}
return $is_pausing;
}
public function isResuming() {
$is_resuming = false;
foreach ($this->getUnprocessedCommands() as $command_object) {
$command = $command_object->getCommand();
switch ($command) {
case HarbormasterBuildCommand::COMMAND_RESTART:
case HarbormasterBuildCommand::COMMAND_RESUME:
$is_resuming = true;
break;
case HarbormasterBuildCommand::COMMAND_PAUSE:
$is_resuming = false;
break;
case HarbormasterBuildCommand::COMMAND_ABORT:
$is_resuming = false;
break;
}
}
return $is_resuming;
}
public function isRestarting() {
$is_restarting = false;
foreach ($this->getUnprocessedCommands() as $command_object) {
$command = $command_object->getCommand();
switch ($command) {
case HarbormasterBuildCommand::COMMAND_RESTART:
$is_restarting = true;
break;
}
}
return $is_restarting;
}
public function isAborting() {
$is_aborting = false;
foreach ($this->getUnprocessedCommands() as $command_object) {
$command = $command_object->getCommand();
switch ($command) {
case HarbormasterBuildCommand::COMMAND_ABORT:
$is_aborting = true;
break;
}
}
return $is_aborting;
}
public function deleteUnprocessedCommands() {
foreach ($this->getUnprocessedCommands() as $key => $command_object) {
$command_object->delete();
unset($this->unprocessedCommands[$key]);
}
return $this;
}
public function canIssueCommand(PhabricatorUser $viewer, $command) {
try {
$this->assertCanIssueCommand($viewer, $command);
return true;
} catch (Exception $ex) {
return false;
}
}
public function assertCanIssueCommand(PhabricatorUser $viewer, $command) {
- $need_edit = false;
+ $plan = $this->getBuildPlan();
+
+ $need_edit = true;
switch ($command) {
case HarbormasterBuildCommand::COMMAND_RESTART:
- break;
case HarbormasterBuildCommand::COMMAND_PAUSE:
case HarbormasterBuildCommand::COMMAND_RESUME:
case HarbormasterBuildCommand::COMMAND_ABORT:
- $need_edit = true;
+ if ($plan->canRunWithoutEditCapability()) {
+ $need_edit = false;
+ }
break;
default:
throw new Exception(
pht(
'Invalid Harbormaster build command "%s".',
$command));
}
// Issuing these commands requires that you be able to edit the build, to
// prevent enemy engineers from sabotaging your builds. See T9614.
if ($need_edit) {
PhabricatorPolicyFilter::requireCapability(
$viewer,
- $this->getBuildPlan(),
+ $plan,
PhabricatorPolicyCapability::CAN_EDIT);
}
}
public function sendMessage(PhabricatorUser $viewer, $command) {
// TODO: This should not be an editor transaction, but there are plans to
// merge BuildCommand into BuildMessage which should moot this. As this
// exists today, it can race against BuildEngine.
// This is a bogus content source, but this whole flow should be obsolete
// soon.
$content_source = PhabricatorContentSource::newForSource(
PhabricatorConsoleContentSource::SOURCECONST);
$editor = id(new HarbormasterBuildTransactionEditor())
->setActor($viewer)
->setContentSource($content_source)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$viewer_phid = $viewer->getPHID();
if (!$viewer_phid) {
$acting_phid = id(new PhabricatorHarbormasterApplication())->getPHID();
$editor->setActingAsPHID($acting_phid);
}
$xaction = id(new HarbormasterBuildTransaction())
->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND)
->setNewValue($command);
$editor->applyTransactions($this, array($xaction));
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new HarbormasterBuildTransactionEditor();
}
public function getApplicationTransactionTemplate() {
return new HarbormasterBuildTransaction();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getBuildable()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getBuildable()->hasAutomaticCapability(
$capability,
$viewer);
}
public function describeAutomaticCapability($capability) {
return pht('A build inherits policies from its buildable.');
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('buildablePHID')
->setType('phid')
->setDescription(pht('PHID of the object this build is building.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('buildPlanPHID')
->setType('phid')
->setDescription(pht('PHID of the build plan being run.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('buildStatus')
->setType('map<string, wild>')
->setDescription(pht('The current status of this build.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('initiatorPHID')
->setType('phid')
->setDescription(pht('The person (or thing) that started this build.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of this build.')),
);
}
public function getFieldValuesForConduit() {
$status = $this->getBuildStatus();
return array(
'buildablePHID' => $this->getBuildablePHID(),
'buildPlanPHID' => $this->getBuildPlanPHID(),
'buildStatus' => array(
'value' => $status,
'name' => HarbormasterBuildStatus::getBuildStatusName($status),
'color.ansi' =>
HarbormasterBuildStatus::getBuildStatusANSIColor($status),
),
'initiatorPHID' => nonempty($this->getInitiatorPHID(), null),
'name' => $this->getName(),
);
}
public function getConduitSearchAttachments() {
return array(
id(new HarbormasterQueryBuildsSearchEngineAttachment())
->setAttachmentKey('querybuilds'),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$viewer = $engine->getViewer();
$this->openTransaction();
$targets = id(new HarbormasterBuildTargetQuery())
->setViewer($viewer)
->withBuildPHIDs(array($this->getPHID()))
->execute();
foreach ($targets as $target) {
$engine->destroyObject($target);
}
$messages = id(new HarbormasterBuildMessageQuery())
->setViewer($viewer)
->withReceiverPHIDs(array($this->getPHID()))
->execute();
foreach ($messages as $message) {
$engine->destroyObject($message);
}
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php b/src/applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php
index b2f566c3e..9e437efab 100644
--- a/src/applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php
@@ -1,262 +1,313 @@
<?php
final class HarbormasterBuildUnitMessage
- extends HarbormasterDAO {
+ extends HarbormasterDAO
+ implements PhabricatorPolicyInterface {
protected $buildTargetPHID;
protected $engine;
protected $namespace;
protected $name;
+ protected $nameIndex;
protected $result;
protected $duration;
protected $properties = array();
private $buildTarget = self::ATTACHABLE;
const FORMAT_TEXT = 'text';
const FORMAT_REMARKUP = 'remarkup';
public static function initializeNewUnitMessage(
HarbormasterBuildTarget $build_target) {
return id(new HarbormasterBuildUnitMessage())
->setBuildTargetPHID($build_target->getPHID());
}
public static function getParameterSpec() {
return array(
'name' => array(
'type' => 'string',
'description' => pht(
'Short test name, like "ExampleTest".'),
),
'result' => array(
'type' => 'string',
'description' => pht(
'Result of the test.'),
),
'namespace' => array(
'type' => 'optional string',
'description' => pht(
'Optional namespace for this test. This is organizational and '.
'is often a class or module name, like "ExampleTestCase".'),
),
'engine' => array(
'type' => 'optional string',
'description' => pht(
'Test engine running the test, like "JavascriptTestEngine". This '.
'primarily prevents collisions between tests with the same name '.
'in different test suites (for example, a Javascript test and a '.
'Python test).'),
),
'duration' => array(
'type' => 'optional float|int',
'description' => pht(
'Runtime duration of the test, in seconds.'),
),
'path' => array(
'type' => 'optional string',
'description' => pht(
'Path to the file where the test is declared, relative to the '.
'project root.'),
),
'coverage' => array(
'type' => 'optional map<string, wild>',
'description' => pht(
'Coverage information for this test.'),
),
'details' => array(
'type' => 'optional string',
'description' => pht(
'Additional human-readable information about the failure.'),
),
'format' => array(
'type' => 'optional string',
'description' => pht(
'Format for the text provided in "details". Valid values are '.
'"text" (default) or "remarkup". This controls how test details '.
'are rendered when shown to users.'),
),
);
}
public static function newFromDictionary(
HarbormasterBuildTarget $build_target,
array $dict) {
$obj = self::initializeNewUnitMessage($build_target);
$spec = self::getParameterSpec();
$spec = ipull($spec, 'type');
// We're just going to ignore extra keys for now, to make it easier to
// add stuff here later on.
$dict = array_select_keys($dict, array_keys($spec));
PhutilTypeSpec::checkMap($dict, $spec);
$obj->setEngine(idx($dict, 'engine', ''));
$obj->setNamespace(idx($dict, 'namespace', ''));
$obj->setName($dict['name']);
$obj->setResult($dict['result']);
$obj->setDuration((float)idx($dict, 'duration'));
$path = idx($dict, 'path');
if (strlen($path)) {
$obj->setProperty('path', $path);
}
$coverage = idx($dict, 'coverage');
if ($coverage) {
$obj->setProperty('coverage', $coverage);
}
$details = idx($dict, 'details');
if ($details) {
$obj->setProperty('details', $details);
}
$format = idx($dict, 'format');
if ($format) {
$obj->setProperty('format', $format);
}
return $obj;
}
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'engine' => 'text255',
'namespace' => 'text255',
'name' => 'text255',
+ 'nameIndex' => 'bytes12',
'result' => 'text32',
'duration' => 'double?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_target' => array(
'columns' => array('buildTargetPHID'),
),
),
) + parent::getConfiguration();
}
public function attachBuildTarget(HarbormasterBuildTarget $build_target) {
$this->buildTarget = $build_target;
return $this;
}
public function getBuildTarget() {
return $this->assertAttached($this->buildTarget);
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getUnitMessageDetails() {
return $this->getProperty('details', '');
}
public function getUnitMessageDetailsFormat() {
return $this->getProperty('format', self::FORMAT_TEXT);
}
public function newUnitMessageDetailsView(
PhabricatorUser $viewer,
$summarize = false) {
$format = $this->getUnitMessageDetailsFormat();
$is_text = ($format !== self::FORMAT_REMARKUP);
$is_remarkup = ($format === self::FORMAT_REMARKUP);
$full_details = $this->getUnitMessageDetails();
if (!strlen($full_details)) {
if ($summarize) {
return null;
}
$details = phutil_tag('em', array(), pht('No details provided.'));
} else if ($summarize) {
if ($is_text) {
$details = id(new PhutilUTF8StringTruncator())
->setMaximumBytes(2048)
->truncateString($full_details);
$details = phutil_split_lines($details);
$limit = 3;
if (count($details) > $limit) {
$details = array_slice($details, 0, $limit);
}
$details = implode('', $details);
} else {
$details = $full_details;
}
} else {
$details = $full_details;
}
require_celerity_resource('harbormaster-css');
$classes = array();
$classes[] = 'harbormaster-unit-details';
if ($is_remarkup) {
$details = new PHUIRemarkupView($viewer, $details);
} else {
$classes[] = 'harbormaster-unit-details-text';
$classes[] = 'PhabricatorMonospaced';
}
return phutil_tag(
'div',
array(
'class' => implode(' ', $classes),
),
$details);
}
public function getUnitMessageDisplayName() {
$name = $this->getName();
$namespace = $this->getNamespace();
if (strlen($namespace)) {
$name = $namespace.'::'.$name;
}
$engine = $this->getEngine();
if (strlen($engine)) {
$name = $engine.' > '.$name;
}
if (!strlen($name)) {
return pht('Nameless Test (%d)', $this->getID());
}
return $name;
}
public function getSortKey() {
$status = $this->getResult();
$sort = HarbormasterUnitStatus::getUnitStatusSort($status);
$parts = array(
$sort,
$this->getEngine(),
$this->getNamespace(),
$this->getName(),
$this->getID(),
);
return implode("\0", $parts);
}
+ public function save() {
+ if ($this->nameIndex === null) {
+ $this->nameIndex = HarbormasterString::newIndex($this->getName());
+ }
+
+ // See T13088. While we're letting installs do online migrations to avoid
+ // downtime, don't populate the "name" column for new writes. New writes
+ // use the "HarbormasterString" table instead.
+ $old_name = $this->name;
+ $this->name = '';
+
+ $caught = null;
+ try {
+ $result = parent::save();
+ } catch (Exception $ex) {
+ $caught = $ex;
+ }
+
+ $this->name = $old_name;
+
+ if ($caught) {
+ throw $caught;
+ }
+
+ return $result;
+ }
+
+
+/* -( PhabricatorPolicyInterface )----------------------------------------- */
+
+
+ public function getCapabilities() {
+ return array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ );
+ }
+
+ public function getPolicy($capability) {
+ switch ($capability) {
+ case PhabricatorPolicyCapability::CAN_VIEW:
+ return PhabricatorPolicies::getMostOpenPolicy();
+ }
+ }
+
+ public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
+ return false;
+ }
+
}
diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php
index 2e379aab2..798201f49 100644
--- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php
+++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php
@@ -1,229 +1,308 @@
<?php
/**
* @task autoplan Autoplans
*/
final class HarbormasterBuildPlan extends HarbormasterDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorSubscribableInterface,
PhabricatorNgramsInterface,
PhabricatorConduitResultInterface,
- PhabricatorProjectInterface {
+ PhabricatorProjectInterface,
+ PhabricatorPolicyCodexInterface {
protected $name;
protected $planStatus;
protected $planAutoKey;
protected $viewPolicy;
protected $editPolicy;
+ protected $properties = array();
const STATUS_ACTIVE = 'active';
const STATUS_DISABLED = 'disabled';
private $buildSteps = self::ATTACHABLE;
public static function initializeNewBuildPlan(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorHarbormasterApplication'))
->executeOne();
$view_policy = $app->getPolicy(
HarbormasterBuildPlanDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(
HarbormasterBuildPlanDefaultEditCapability::CAPABILITY);
return id(new HarbormasterBuildPlan())
->setName('')
->setPlanStatus(self::STATUS_ACTIVE)
->attachBuildSteps(array())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
+ self::CONFIG_SERIALIZATION => array(
+ 'properties' => self::SERIALIZATION_JSON,
+ ),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort128',
'planStatus' => 'text32',
'planAutoKey' => 'text32?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_status' => array(
'columns' => array('planStatus'),
),
'key_name' => array(
'columns' => array('name'),
),
'key_planautokey' => array(
'columns' => array('planAutoKey'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
HarbormasterBuildPlanPHIDType::TYPECONST);
}
public function attachBuildSteps(array $steps) {
assert_instances_of($steps, 'HarbormasterBuildStep');
$this->buildSteps = $steps;
return $this;
}
public function getBuildSteps() {
return $this->assertAttached($this->buildSteps);
}
public function isDisabled() {
return ($this->getPlanStatus() == self::STATUS_DISABLED);
}
+ public function getURI() {
+ return urisprintf(
+ '/harbormaster/plan/%s/',
+ $this->getID());
+ }
+
+ public function getObjectName() {
+ return pht('Plan %d', $this->getID());
+ }
+
+ public function getPlanProperty($key, $default = null) {
+ return idx($this->properties, $key, $default);
+ }
+
+ public function setPlanProperty($key, $value) {
+ $this->properties[$key] = $value;
+ return $this;
+ }
+
/* -( Autoplans )---------------------------------------------------------- */
public function isAutoplan() {
return ($this->getPlanAutoKey() !== null);
}
public function getAutoplan() {
if (!$this->isAutoplan()) {
return null;
}
return HarbormasterBuildAutoplan::getAutoplan($this->getPlanAutoKey());
}
public function canRunManually() {
if ($this->isAutoplan()) {
return false;
}
return true;
}
-
public function getName() {
$autoplan = $this->getAutoplan();
if ($autoplan) {
return $autoplan->getAutoplanName();
}
return parent::getName();
}
+ public function hasRunCapability(PhabricatorUser $viewer) {
+ try {
+ $this->assertHasRunCapability($viewer);
+ return true;
+ } catch (PhabricatorPolicyException $ex) {
+ return false;
+ }
+ }
+
+ public function canRunWithoutEditCapability() {
+ $runnable = HarbormasterBuildPlanBehavior::BEHAVIOR_RUNNABLE;
+ $if_viewable = HarbormasterBuildPlanBehavior::RUNNABLE_IF_VIEWABLE;
+
+ $option = HarbormasterBuildPlanBehavior::getBehavior($runnable)
+ ->getPlanOption($this);
+
+ return ($option->getKey() === $if_viewable);
+ }
+
+ public function assertHasRunCapability(PhabricatorUser $viewer) {
+ if ($this->canRunWithoutEditCapability()) {
+ $capability = PhabricatorPolicyCapability::CAN_VIEW;
+ } else {
+ $capability = PhabricatorPolicyCapability::CAN_EDIT;
+ }
+
+ PhabricatorPolicyFilter::requireCapability(
+ $viewer,
+ $this,
+ $capability);
+ }
+
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return false;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new HarbormasterBuildPlanEditor();
}
public function getApplicationTransactionTemplate() {
return new HarbormasterBuildPlanTransaction();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if ($this->isAutoplan()) {
return PhabricatorPolicies::getMostOpenPolicy();
}
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->isAutoplan()) {
return PhabricatorPolicies::POLICY_NOONE;
}
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
$messages = array();
switch ($capability) {
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->isAutoplan()) {
$messages[] = pht(
'This is an autoplan (a builtin plan provided by an application) '.
'so it can not be edited.');
}
break;
}
return $messages;
}
/* -( PhabricatorNgramsInterface )----------------------------------------- */
public function newNgrams() {
return array(
id(new HarbormasterBuildPlanNameNgrams())
->setValue($this->getName()),
);
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of this build plan.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('status')
->setType('map<string, wild>')
->setDescription(pht('The current status of this build plan.')),
+ id(new PhabricatorConduitSearchFieldSpecification())
+ ->setKey('behaviors')
+ ->setType('map<string, string>')
+ ->setDescription(pht('Behavior configuration for the build plan.')),
);
}
public function getFieldValuesForConduit() {
+ $behavior_map = array();
+
+ $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
+ foreach ($behaviors as $behavior) {
+ $option = $behavior->getPlanOption($this);
+
+ $behavior_map[$behavior->getKey()] = array(
+ 'value' => $option->getKey(),
+ );
+ }
+
return array(
'name' => $this->getName(),
'status' => array(
'value' => $this->getPlanStatus(),
),
+ 'behaviors' => $behavior_map,
);
}
public function getConduitSearchAttachments() {
return array();
}
+
+/* -( PhabricatorPolicyCodexInterface )------------------------------------ */
+
+
+ public function newPolicyCodex() {
+ return new HarbormasterBuildPlanPolicyCodex();
+ }
+
}
diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php
index 130471e21..6cd286343 100644
--- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php
+++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php
@@ -1,80 +1,18 @@
<?php
final class HarbormasterBuildPlanTransaction
- extends PhabricatorApplicationTransaction {
-
- const TYPE_NAME = 'harbormaster:name';
- const TYPE_STATUS = 'harbormaster:status';
+ extends PhabricatorModularTransaction {
public function getApplicationName() {
return 'harbormaster';
}
public function getApplicationTransactionType() {
return HarbormasterBuildPlanPHIDType::TYPECONST;
}
- public function getIcon() {
- $old = $this->getOldValue();
- $new = $this->getNewValue();
-
- switch ($this->getTransactionType()) {
- case self::TYPE_NAME:
- if ($old === null) {
- return 'fa-plus';
- }
- break;
- }
-
- return parent::getIcon();
- }
-
- public function getColor() {
- $old = $this->getOldValue();
- $new = $this->getNewValue();
-
- switch ($this->getTransactionType()) {
- case self::TYPE_NAME:
- if ($old === null) {
- return 'green';
- }
- break;
- }
-
- return parent::getIcon();
- }
-
- public function getTitle() {
- $old = $this->getOldValue();
- $new = $this->getNewValue();
- $author_handle = $this->renderHandleLink($this->getAuthorPHID());
-
- switch ($this->getTransactionType()) {
- case self::TYPE_NAME:
- if ($old === null) {
- return pht(
- '%s created this build plan.',
- $author_handle);
- } else {
- return pht(
- '%s renamed this build plan from "%s" to "%s".',
- $author_handle,
- $old,
- $new);
- }
- case self::TYPE_STATUS:
- if ($new == HarbormasterBuildPlan::STATUS_DISABLED) {
- return pht(
- '%s disabled this build plan.',
- $author_handle);
- } else {
- return pht(
- '%s enabled this build plan.',
- $author_handle);
- }
- }
-
- return parent::getTitle();
+ public function getBaseTransactionClass() {
+ return 'HarbormasterBuildPlanTransactionType';
}
}
diff --git a/src/applications/harbormaster/view/HarbormasterBuildView.php b/src/applications/harbormaster/view/HarbormasterBuildView.php
new file mode 100644
index 000000000..54f5abe09
--- /dev/null
+++ b/src/applications/harbormaster/view/HarbormasterBuildView.php
@@ -0,0 +1,67 @@
+<?php
+
+final class HarbormasterBuildView
+ extends AphrontView {
+
+ private $builds = array();
+
+ public function setBuilds(array $builds) {
+ assert_instances_of($builds, 'HarbormasterBuild');
+ $this->builds = $builds;
+ return $this;
+ }
+
+ public function getBuilds() {
+ return $this->builds;
+ }
+
+ public function render() {
+ return $this->newObjectList();
+ }
+
+ public function newObjectList() {
+ $viewer = $this->getViewer();
+ $builds = $this->getBuilds();
+
+ $buildables = mpull($builds, 'getBuildable');
+ $object_phids = mpull($buildables, 'getBuildablePHID');
+ $initiator_phids = mpull($builds, 'getInitiatorPHID');
+ $phids = array_mergev(array($initiator_phids, $object_phids));
+ $phids = array_unique(array_filter($phids));
+
+ $handles = $viewer->loadHandles($phids);
+
+ $list = new PHUIObjectItemListView();
+ foreach ($builds as $build) {
+ $id = $build->getID();
+ $initiator = $handles[$build->getInitiatorPHID()];
+ $buildable_object = $handles[$build->getBuildable()->getBuildablePHID()];
+
+ $item = id(new PHUIObjectItemView())
+ ->setViewer($viewer)
+ ->setObject($build)
+ ->setObjectName($build->getObjectName())
+ ->setHeader($build->getName())
+ ->setHref($build->getURI())
+ ->setEpoch($build->getDateCreated())
+ ->addAttribute($buildable_object->getName());
+
+ if ($initiator) {
+ $item->addByline($initiator->renderLink());
+ }
+
+ $status = $build->getBuildStatus();
+
+ $status_icon = HarbormasterBuildStatus::getBuildStatusIcon($status);
+ $status_color = HarbormasterBuildStatus::getBuildStatusColor($status);
+ $status_label = HarbormasterBuildStatus::getBuildStatusName($status);
+
+ $item->setStatusIcon("{$status_icon} {$status_color}", $status_label);
+
+ $list->addItem($item);
+ }
+
+ return $list;
+ }
+
+}
diff --git a/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanBehaviorTransaction.php b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanBehaviorTransaction.php
new file mode 100644
index 000000000..7a65eefdf
--- /dev/null
+++ b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanBehaviorTransaction.php
@@ -0,0 +1,127 @@
+<?php
+
+final class HarbormasterBuildPlanBehaviorTransaction
+ extends HarbormasterBuildPlanTransactionType {
+
+ const TRANSACTIONTYPE = 'behavior';
+
+ public function generateOldValue($object) {
+ $behavior = $this->getBehavior();
+ return $behavior->getPlanOption($object)->getKey();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $key = $this->getStorageKey();
+ return $object->setPlanProperty($key, $value);
+ }
+
+ public function getTitle() {
+ $old_value = $this->getOldValue();
+ $new_value = $this->getNewValue();
+
+ $behavior = $this->getBehavior();
+ if ($behavior) {
+ $behavior_name = $behavior->getName();
+
+ $options = $behavior->getOptions();
+ if (isset($options[$old_value])) {
+ $old_value = $options[$old_value]->getName();
+ }
+
+ if (isset($options[$new_value])) {
+ $new_value = $options[$new_value]->getName();
+ }
+ } else {
+ $behavior_name = $this->getBehaviorKey();
+ }
+
+ return pht(
+ '%s changed the %s behavior for this plan from %s to %s.',
+ $this->renderAuthor(),
+ $this->renderValue($behavior_name),
+ $this->renderValue($old_value),
+ $this->renderValue($new_value));
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+
+ $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
+ $behaviors = mpull($behaviors, null, 'getKey');
+
+ foreach ($xactions as $xaction) {
+ $key = $this->getBehaviorKeyForTransaction($xaction);
+
+ if (!isset($behaviors[$key])) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'No behavior with key "%s" exists. Valid keys are: %s.',
+ $key,
+ implode(', ', array_keys($behaviors))),
+ $xaction);
+ continue;
+ }
+
+ $behavior = $behaviors[$key];
+ $options = $behavior->getOptions();
+
+ $storage_key = HarbormasterBuildPlanBehavior::getStorageKeyForBehaviorKey(
+ $key);
+ $old = $object->getPlanProperty($storage_key);
+ $new = $xaction->getNewValue();
+
+ if ($old === $new) {
+ continue;
+ }
+
+ if (!isset($options[$new])) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Value "%s" is not a valid option for behavior "%s". Valid '.
+ 'options are: %s.',
+ $new,
+ $key,
+ implode(', ', array_keys($options))),
+ $xaction);
+ continue;
+ }
+ }
+
+ return $errors;
+ }
+
+ public function getTransactionTypeForConduit($xaction) {
+ return 'behavior';
+ }
+
+ public function getFieldValuesForConduit($xaction, $data) {
+ return array(
+ 'key' => $this->getBehaviorKeyForTransaction($xaction),
+ 'old' => $xaction->getOldValue(),
+ 'new' => $xaction->getNewValue(),
+ );
+ }
+
+ private function getBehaviorKeyForTransaction(
+ PhabricatorApplicationTransaction $xaction) {
+ $metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey();
+ return $xaction->getMetadataValue($metadata_key);
+ }
+
+ private function getBehaviorKey() {
+ $metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey();
+ return $this->getMetadataValue($metadata_key);
+ }
+
+ private function getBehavior() {
+ $behavior_key = $this->getBehaviorKey();
+ $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
+ return idx($behaviors, $behavior_key);
+ }
+
+ private function getStorageKey() {
+ return HarbormasterBuildPlanBehavior::getStorageKeyForBehaviorKey(
+ $this->getBehaviorKey());
+ }
+
+}
diff --git a/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanNameTransaction.php b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanNameTransaction.php
new file mode 100644
index 000000000..30fdbe72c
--- /dev/null
+++ b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanNameTransaction.php
@@ -0,0 +1,46 @@
+<?php
+
+final class HarbormasterBuildPlanNameTransaction
+ extends HarbormasterBuildPlanTransactionType {
+
+ const TRANSACTIONTYPE = 'harbormaster:name';
+
+ public function generateOldValue($object) {
+ return $object->getName();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setName($value);
+ }
+
+ public function getTitle() {
+ return pht(
+ '%s renamed this build plan 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('You must choose a name for your build plan.'));
+ }
+
+ return $errors;
+ }
+
+ public function getTransactionTypeForConduit($xaction) {
+ return 'name';
+ }
+
+ public function getFieldValuesForConduit($xaction, $data) {
+ return array(
+ 'old' => $xaction->getOldValue(),
+ 'new' => $xaction->getNewValue(),
+ );
+ }
+
+}
diff --git a/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanStatusTransaction.php b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanStatusTransaction.php
new file mode 100644
index 000000000..e1c72b418
--- /dev/null
+++ b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanStatusTransaction.php
@@ -0,0 +1,67 @@
+<?php
+
+final class HarbormasterBuildPlanStatusTransaction
+ extends HarbormasterBuildPlanTransactionType {
+
+ const TRANSACTIONTYPE = 'harbormaster:status';
+
+ public function generateOldValue($object) {
+ return $object->getPlanStatus();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setPlanStatus($value);
+ }
+
+ public function getTitle() {
+ $new = $this->getNewValue();
+ if ($new === HarbormasterBuildPlan::STATUS_DISABLED) {
+ return pht(
+ '%s disabled this build plan.',
+ $this->renderAuthor());
+ } else {
+ return pht(
+ '%s enabled this build plan.',
+ $this->renderAuthor());
+ }
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+
+ $options = array(
+ HarbormasterBuildPlan::STATUS_DISABLED,
+ HarbormasterBuildPlan::STATUS_ACTIVE,
+ );
+ $options = array_fuse($options);
+
+ foreach ($xactions as $xaction) {
+ $new = $xaction->getNewValue();
+
+ if (!isset($options[$new])) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Status "%s" is not a valid build plan status. Valid '.
+ 'statuses are: %s.',
+ $new,
+ implode(', ', $options)));
+ continue;
+ }
+
+ }
+
+ return $errors;
+ }
+
+ public function getTransactionTypeForConduit($xaction) {
+ return 'status';
+ }
+
+ public function getFieldValuesForConduit($xaction, $data) {
+ return array(
+ 'old' => $xaction->getOldValue(),
+ 'new' => $xaction->getNewValue(),
+ );
+ }
+
+}
diff --git a/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanTransactionType.php b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanTransactionType.php
new file mode 100644
index 000000000..5545d1de3
--- /dev/null
+++ b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanTransactionType.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class HarbormasterBuildPlanTransactionType
+ extends PhabricatorModularTransactionType {}
diff --git a/src/applications/herald/action/HeraldAction.php b/src/applications/herald/action/HeraldAction.php
index 04884a94d..a9740d173 100644
--- a/src/applications/herald/action/HeraldAction.php
+++ b/src/applications/herald/action/HeraldAction.php
@@ -1,404 +1,408 @@
<?php
abstract class HeraldAction extends Phobject {
private $adapter;
private $viewer;
private $applyLog = array();
const STANDARD_NONE = 'standard.none';
const STANDARD_PHID_LIST = 'standard.phid.list';
const STANDARD_TEXT = 'standard.text';
const STANDARD_REMARKUP = 'standard.remarkup';
const DO_STANDARD_EMPTY = 'do.standard.empty';
const DO_STANDARD_NO_EFFECT = 'do.standard.no-effect';
const DO_STANDARD_INVALID = 'do.standard.invalid';
const DO_STANDARD_UNLOADABLE = 'do.standard.unloadable';
const DO_STANDARD_PERMISSION = 'do.standard.permission';
const DO_STANDARD_INVALID_ACTION = 'do.standard.invalid-action';
const DO_STANDARD_WRONG_RULE_TYPE = 'do.standard.wrong-rule-type';
const DO_STANDARD_FORBIDDEN = 'do.standard.forbidden';
abstract public function getHeraldActionName();
abstract public function supportsObject($object);
abstract public function supportsRuleType($rule_type);
abstract public function applyEffect($object, HeraldEffect $effect);
abstract public function renderActionDescription($value);
public function getRequiredAdapterStates() {
return array();
}
protected function renderActionEffectDescription($type, $data) {
return null;
}
public function getActionGroupKey() {
return null;
}
public function getActionsForObject($object) {
return array($this->getActionConstant() => $this);
}
protected function getDatasource() {
throw new PhutilMethodNotImplementedException();
}
protected function getDatasourceValueMap() {
return null;
}
public function getHeraldActionStandardType() {
throw new PhutilMethodNotImplementedException();
}
public function getHeraldActionValueType() {
switch ($this->getHeraldActionStandardType()) {
case self::STANDARD_NONE:
return new HeraldEmptyFieldValue();
case self::STANDARD_TEXT:
return new HeraldTextFieldValue();
case self::STANDARD_REMARKUP:
return new HeraldRemarkupFieldValue();
case self::STANDARD_PHID_LIST:
$tokenizer = id(new HeraldTokenizerFieldValue())
->setKey($this->getHeraldActionName())
->setDatasource($this->getDatasource());
$value_map = $this->getDatasourceValueMap();
if ($value_map !== null) {
$tokenizer->setValueMap($value_map);
}
return $tokenizer;
}
throw new PhutilMethodNotImplementedException();
}
public function willSaveActionValue($value) {
try {
$type = $this->getHeraldActionStandardType();
} catch (PhutilMethodNotImplementedException $ex) {
return $value;
}
switch ($type) {
case self::STANDARD_PHID_LIST:
return array_keys($value);
}
return $value;
}
public function getEditorValue(PhabricatorUser $viewer, $target) {
try {
$type = $this->getHeraldActionStandardType();
} catch (PhutilMethodNotImplementedException $ex) {
return $target;
}
switch ($type) {
case self::STANDARD_PHID_LIST:
$datasource = $this->getDatasource();
if (!$datasource) {
return array();
}
return $datasource
->setViewer($viewer)
->getWireTokens($target);
}
return $target;
}
final public function setAdapter(HeraldAdapter $adapter) {
$this->adapter = $adapter;
return $this;
}
final public function getAdapter() {
return $this->adapter;
}
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
final public function getViewer() {
return $this->viewer;
}
final public function getActionConstant() {
return $this->getPhobjectClassConstant('ACTIONCONST', 64);
}
final public static function getAllActions() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getActionConstant')
->execute();
}
protected function logEffect($type, $data = null) {
if (!is_string($type)) {
throw new Exception(
pht(
'Effect type passed to "%s" must be a scalar string.',
'logEffect()'));
}
$this->applyLog[] = array(
'type' => $type,
'data' => $data,
);
return $this;
}
final public function getApplyTranscript(HeraldEffect $effect) {
$context = $this->applyLog;
$this->applyLog = array();
return new HeraldApplyTranscript($effect, true, $context);
}
protected function getActionEffectMap() {
throw new PhutilMethodNotImplementedException();
}
private function getActionEffectSpec($type) {
$map = $this->getActionEffectMap() + $this->getStandardEffectMap();
return idx($map, $type, array());
}
final public function renderActionEffectIcon($type, $data) {
$map = $this->getActionEffectSpec($type);
return idx($map, 'icon');
}
final public function renderActionEffectColor($type, $data) {
$map = $this->getActionEffectSpec($type);
return idx($map, 'color');
}
final public function renderActionEffectName($type, $data) {
$map = $this->getActionEffectSpec($type);
return idx($map, 'name');
}
protected function renderHandleList($phids) {
if (!is_array($phids)) {
return pht('(Invalid List)');
}
return $this->getViewer()
->renderHandleList($phids)
->setAsInline(true)
->render();
}
protected function loadStandardTargets(
array $phids,
array $allowed_types,
array $current_value) {
$phids = array_fuse($phids);
if (!$phids) {
$this->logEffect(self::DO_STANDARD_EMPTY);
}
$current_value = array_fuse($current_value);
$no_effect = array();
foreach ($phids as $phid) {
if (isset($current_value[$phid])) {
$no_effect[] = $phid;
unset($phids[$phid]);
}
}
if ($no_effect) {
$this->logEffect(self::DO_STANDARD_NO_EFFECT, $no_effect);
}
if (!$phids) {
return;
}
$allowed_types = array_fuse($allowed_types);
$invalid = array();
foreach ($phids as $phid) {
$type = phid_get_type($phid);
if ($type == PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
$invalid[] = $phid;
unset($phids[$phid]);
continue;
}
if ($allowed_types && empty($allowed_types[$type])) {
$invalid[] = $phid;
unset($phids[$phid]);
continue;
}
}
if ($invalid) {
$this->logEffect(self::DO_STANDARD_INVALID, $invalid);
}
if (!$phids) {
return;
}
$targets = id(new PhabricatorObjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($phids)
->execute();
$targets = mpull($targets, null, 'getPHID');
$unloadable = array();
foreach ($phids as $phid) {
if (empty($targets[$phid])) {
$unloadable[] = $phid;
unset($phids[$phid]);
}
}
if ($unloadable) {
$this->logEffect(self::DO_STANDARD_UNLOADABLE, $unloadable);
}
if (!$phids) {
return;
}
$adapter = $this->getAdapter();
$object = $adapter->getObject();
if ($object instanceof PhabricatorPolicyInterface) {
$no_permission = array();
foreach ($targets as $phid => $target) {
if (!($target instanceof PhabricatorUser)) {
continue;
}
$can_view = PhabricatorPolicyFilter::hasCapability(
$target,
$object,
PhabricatorPolicyCapability::CAN_VIEW);
if ($can_view) {
continue;
}
$no_permission[] = $phid;
unset($targets[$phid]);
}
}
if ($no_permission) {
$this->logEffect(self::DO_STANDARD_PERMISSION, $no_permission);
}
return $targets;
}
protected function getStandardEffectMap() {
return array(
self::DO_STANDARD_EMPTY => array(
'icon' => 'fa-ban',
'color' => 'grey',
'name' => pht('No Targets'),
),
self::DO_STANDARD_NO_EFFECT => array(
'icon' => 'fa-circle-o',
'color' => 'grey',
'name' => pht('No Effect'),
),
self::DO_STANDARD_INVALID => array(
'icon' => 'fa-ban',
'color' => 'red',
'name' => pht('Invalid Targets'),
),
self::DO_STANDARD_UNLOADABLE => array(
'icon' => 'fa-ban',
'color' => 'red',
'name' => pht('Unloadable Targets'),
),
self::DO_STANDARD_PERMISSION => array(
'icon' => 'fa-lock',
'color' => 'red',
'name' => pht('No Permission'),
),
self::DO_STANDARD_INVALID_ACTION => array(
'icon' => 'fa-ban',
'color' => 'red',
'name' => pht('Invalid Action'),
),
self::DO_STANDARD_WRONG_RULE_TYPE => array(
'icon' => 'fa-ban',
'color' => 'red',
'name' => pht('Wrong Rule Type'),
),
self::DO_STANDARD_FORBIDDEN => array(
'icon' => 'fa-ban',
'color' => 'violet',
'name' => pht('Forbidden'),
),
);
}
final public function renderEffectDescription($type, $data) {
$result = $this->renderActionEffectDescription($type, $data);
if ($result !== null) {
return $result;
}
switch ($type) {
case self::DO_STANDARD_EMPTY:
return pht(
'This action specifies no targets.');
case self::DO_STANDARD_NO_EFFECT:
if ($data && is_array($data)) {
return pht(
'This action has no effect on %s target(s): %s.',
phutil_count($data),
$this->renderHandleList($data));
} else {
return pht('This action has no effect.');
}
case self::DO_STANDARD_INVALID:
return pht(
'%s target(s) are invalid or of the wrong type: %s.',
phutil_count($data),
$this->renderHandleList($data));
case self::DO_STANDARD_UNLOADABLE:
return pht(
'%s target(s) could not be loaded: %s.',
phutil_count($data),
$this->renderHandleList($data));
case self::DO_STANDARD_PERMISSION:
return pht(
'%s target(s) do not have permission to see this object: %s.',
phutil_count($data),
$this->renderHandleList($data));
case self::DO_STANDARD_INVALID_ACTION:
return pht(
'No implementation is available for rule "%s".',
$data);
case self::DO_STANDARD_WRONG_RULE_TYPE:
return pht(
'This action does not support rules of type "%s".',
$data);
case self::DO_STANDARD_FORBIDDEN:
return HeraldStateReasons::getExplanation($data);
}
return null;
}
+ public function getPHIDsAffectedByAction(HeraldActionRecord $record) {
+ return array();
+ }
+
}
diff --git a/src/applications/herald/action/HeraldCallWebhookAction.php b/src/applications/herald/action/HeraldCallWebhookAction.php
index 953958e5c..186a7a741 100644
--- a/src/applications/herald/action/HeraldCallWebhookAction.php
+++ b/src/applications/herald/action/HeraldCallWebhookAction.php
@@ -1,66 +1,70 @@
<?php
final class HeraldCallWebhookAction extends HeraldAction {
const ACTIONCONST = 'webhook';
const DO_WEBHOOK = 'do.call-webhook';
public function getHeraldActionName() {
return pht('Call webhooks');
}
public function getActionGroupKey() {
return HeraldUtilityActionGroup::ACTIONGROUPKEY;
}
public function supportsObject($object) {
if (!$this->getAdapter()->supportsWebhooks()) {
return false;
}
return true;
}
public function supportsRuleType($rule_type) {
return ($rule_type !== HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
}
public function applyEffect($object, HeraldEffect $effect) {
$adapter = $this->getAdapter();
$rule = $effect->getRule();
$target = $effect->getTarget();
foreach ($target as $webhook_phid) {
$adapter->queueWebhook($webhook_phid, $rule->getPHID());
}
$this->logEffect(self::DO_WEBHOOK, $target);
}
public function getHeraldActionStandardType() {
return self::STANDARD_PHID_LIST;
}
protected function getActionEffectMap() {
return array(
self::DO_WEBHOOK => array(
'icon' => 'fa-cloud-upload',
'color' => 'green',
'name' => pht('Called Webhooks'),
),
);
}
public function renderActionDescription($value) {
return pht('Call webhooks: %s.', $this->renderHandleList($value));
}
protected function renderActionEffectDescription($type, $data) {
return pht('Called webhooks: %s.', $this->renderHandleList($data));
}
protected function getDatasource() {
return new HeraldWebhookDatasource();
}
+ public function getPHIDsAffectedByAction(HeraldActionRecord $record) {
+ return $record->getTarget();
+ }
+
}
diff --git a/src/applications/herald/action/HeraldCommentAction.php b/src/applications/herald/action/HeraldCommentAction.php
index fa52ba1f5..f8b8fbe81 100644
--- a/src/applications/herald/action/HeraldCommentAction.php
+++ b/src/applications/herald/action/HeraldCommentAction.php
@@ -1,79 +1,76 @@
<?php
final class HeraldCommentAction extends HeraldAction {
const ACTIONCONST = 'comment';
const DO_COMMENT = 'do.comment';
public function getHeraldActionName() {
return pht('Add comment');
}
public function getActionGroupKey() {
return HeraldUtilityActionGroup::ACTIONGROUPKEY;
}
public function supportsObject($object) {
if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
return false;
}
$xaction = $object->getApplicationTransactionTemplate();
- try {
- $comment = $xaction->getApplicationTransactionCommentObject();
- if (!$comment) {
- return false;
- }
- } catch (PhutilMethodNotImplementedException $ex) {
+
+ $comment = $xaction->getApplicationTransactionCommentObject();
+ if (!$comment) {
return false;
}
return true;
}
public function supportsRuleType($rule_type) {
return ($rule_type != HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
}
public function applyEffect($object, HeraldEffect $effect) {
$adapter = $this->getAdapter();
$comment_text = $effect->getTarget();
$xaction = $adapter->newTransaction()
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT);
$comment = $xaction->getApplicationTransactionCommentObject()
->setContent($comment_text);
$xaction->attachComment($comment);
$adapter->queueTransaction($xaction);
$this->logEffect(self::DO_COMMENT, $comment_text);
}
public function getHeraldActionStandardType() {
return self::STANDARD_REMARKUP;
}
protected function getActionEffectMap() {
return array(
self::DO_COMMENT => array(
'icon' => 'fa-comment',
'color' => 'blue',
'name' => pht('Added Comment'),
),
);
}
public function renderActionDescription($value) {
$summary = PhabricatorMarkupEngine::summarize($value);
return pht('Add comment: %s', $summary);
}
protected function renderActionEffectDescription($type, $data) {
$summary = PhabricatorMarkupEngine::summarize($data);
return pht('Added a comment: %s', $summary);
}
}
diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php
index 502d0fc69..437851691 100644
--- a/src/applications/herald/adapter/HeraldAdapter.php
+++ b/src/applications/herald/adapter/HeraldAdapter.php
@@ -1,1244 +1,1257 @@
<?php
abstract class HeraldAdapter extends Phobject {
const CONDITION_CONTAINS = 'contains';
const CONDITION_NOT_CONTAINS = '!contains';
const CONDITION_GREATER = 'greater'; //c4s custo
const CONDITION_LESS = 'less'; //c4s custo
const CONDITION_IS = 'is';
const CONDITION_IS_NOT = '!is';
const CONDITION_IS_ANY = 'isany';
const CONDITION_IS_NOT_ANY = '!isany';
const CONDITION_INCLUDE_ALL = 'all';
const CONDITION_INCLUDE_ANY = 'any';
const CONDITION_INCLUDE_NONE = 'none';
const CONDITION_IS_ME = 'me';
const CONDITION_IS_NOT_ME = '!me';
const CONDITION_REGEXP = 'regexp';
const CONDITION_NOT_REGEXP = '!regexp';
const CONDITION_RULE = 'conditions';
const CONDITION_NOT_RULE = '!conditions';
const CONDITION_EXISTS = 'exists';
const CONDITION_NOT_EXISTS = '!exists';
const CONDITION_UNCONDITIONALLY = 'unconditionally';
const CONDITION_NEVER = 'never';
const CONDITION_REGEXP_PAIR = 'regexp-pair';
const CONDITION_HAS_BIT = 'bit';
const CONDITION_NOT_BIT = '!bit';
const CONDITION_IS_TRUE = 'true';
const CONDITION_IS_FALSE = 'false';
private $contentSource;
private $isNewObject;
private $applicationEmail;
private $appliedTransactions = array();
private $queuedTransactions = array();
private $emailPHIDs = array();
private $forcedEmailPHIDs = array();
private $fieldMap;
private $actionMap;
private $edgeCache = array();
private $forbiddenActions = array();
private $viewer;
private $mustEncryptReasons = array();
private $actingAsPHID;
private $webhookMap = array();
public function getEmailPHIDs() {
return array_values($this->emailPHIDs);
}
public function getForcedEmailPHIDs() {
return array_values($this->forcedEmailPHIDs);
}
final public function setActingAsPHID($acting_as_phid) {
$this->actingAsPHID = $acting_as_phid;
return $this;
}
final public function getActingAsPHID() {
return $this->actingAsPHID;
}
public function addEmailPHID($phid, $force) {
$this->emailPHIDs[$phid] = $phid;
if ($force) {
$this->forcedEmailPHIDs[$phid] = $phid;
}
return $this;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
// See PHI276. Normally, Herald runs without regard for policy checks.
// However, we use a real viewer during test console runs: this makes
// intracluster calls to Diffusion APIs work even if web nodes don't
// have privileged credentials.
if ($this->viewer) {
return $this->viewer;
}
return PhabricatorUser::getOmnipotentUser();
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function getContentSource() {
return $this->contentSource;
}
public function getIsNewObject() {
if (is_bool($this->isNewObject)) {
return $this->isNewObject;
}
throw new Exception(
pht(
'You must %s to a boolean first!',
'setIsNewObject()'));
}
public function setIsNewObject($new) {
$this->isNewObject = (bool)$new;
return $this;
}
public function supportsApplicationEmail() {
return false;
}
public function setApplicationEmail(
PhabricatorMetaMTAApplicationEmail $email) {
$this->applicationEmail = $email;
return $this;
}
public function getApplicationEmail() {
return $this->applicationEmail;
}
public function getPHID() {
return $this->getObject()->getPHID();
}
abstract public function getHeraldName();
public function getHeraldField($field_key) {
return $this->requireFieldImplementation($field_key)
->getHeraldFieldValue($this->getObject());
}
public function applyHeraldEffects(array $effects) {
assert_instances_of($effects, 'HeraldEffect');
$result = array();
foreach ($effects as $effect) {
$result[] = $this->applyStandardEffect($effect);
}
return $result;
}
public function isAvailableToUser(PhabricatorUser $viewer) {
$applications = id(new PhabricatorApplicationQuery())
->setViewer($viewer)
->withInstalled(true)
->withClasses(array($this->getAdapterApplicationClass()))
->execute();
return !empty($applications);
}
/**
* Set the list of transactions which just took effect.
*
* These transactions are set by @{class:PhabricatorApplicationEditor}
* automatically, before it invokes Herald.
*
* @param list<PhabricatorApplicationTransaction> List of transactions.
* @return this
*/
final public function setAppliedTransactions(array $xactions) {
assert_instances_of($xactions, 'PhabricatorApplicationTransaction');
$this->appliedTransactions = $xactions;
return $this;
}
/**
* Get a list of transactions which just took effect.
*
* When an object is edited normally, transactions are applied and then
* Herald executes. You can call this method to examine the transactions
* if you want to react to them.
*
* @return list<PhabricatorApplicationTransaction> List of transactions.
*/
final public function getAppliedTransactions() {
return $this->appliedTransactions;
}
- public function queueTransaction($transaction) {
+ final public function queueTransaction(
+ PhabricatorApplicationTransaction $transaction) {
$this->queuedTransactions[] = $transaction;
}
- public function getQueuedTransactions() {
+ final public function getQueuedTransactions() {
return $this->queuedTransactions;
}
- public function newTransaction() {
+ final public function newTransaction() {
$object = $this->newObject();
if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
throw new Exception(
pht(
'Unable to build a new transaction for adapter object; it does '.
'not implement "%s".',
'PhabricatorApplicationTransactionInterface'));
}
- return $object->getApplicationTransactionTemplate();
+ $xaction = $object->getApplicationTransactionTemplate();
+
+ if (!($xaction instanceof PhabricatorApplicationTransaction)) {
+ throw new Exception(
+ pht(
+ 'Expected object (of class "%s") to return a transaction template '.
+ '(of class "%s"), but it returned something else ("%s").',
+ get_class($object),
+ 'PhabricatorApplicationTransaction',
+ phutil_describe_type($xaction)));
+ }
+
+ return $xaction;
}
/**
* NOTE: You generally should not override this; it exists to support legacy
* adapters which had hard-coded content types.
*/
public function getAdapterContentType() {
return get_class($this);
}
abstract public function getAdapterContentName();
abstract public function getAdapterContentDescription();
abstract public function getAdapterApplicationClass();
abstract public function getObject();
/**
* Return a new characteristic object for this adapter.
*
* The adapter will use this object to test for interfaces, generate
* transactions, and interact with custom fields.
*
* Adapters must return an object from this method to enable custom
* field rules and various implicit actions.
*
* Normally, you'll return an empty version of the adapted object:
*
* return new ApplicationObject();
*
* @return null|object Template object.
*/
protected function newObject() {
return null;
}
public function supportsRuleType($rule_type) {
return false;
}
public function canTriggerOnObject($object) {
return false;
}
public function isTestAdapterForObject($object) {
return false;
}
public function canCreateTestAdapterForObject($object) {
return $this->isTestAdapterForObject($object);
}
public function newTestAdapter(PhabricatorUser $viewer, $object) {
return id(clone $this)
->setObject($object);
}
public function getAdapterTestDescription() {
return null;
}
public function explainValidTriggerObjects() {
return pht('This adapter can not trigger on objects.');
}
public function getTriggerObjectPHIDs() {
return array($this->getPHID());
}
public function getAdapterSortKey() {
return sprintf(
'%08d%s',
$this->getAdapterSortOrder(),
$this->getAdapterContentName());
}
public function getAdapterSortOrder() {
return 1000;
}
/* -( Fields )------------------------------------------------------------- */
private function getFieldImplementationMap() {
if ($this->fieldMap === null) {
// We can't use PhutilClassMapQuery here because field expansion
// depends on the adapter and object.
$object = $this->getObject();
$map = array();
$all = HeraldField::getAllFields();
foreach ($all as $key => $field) {
$field = id(clone $field)->setAdapter($this);
if (!$field->supportsObject($object)) {
continue;
}
$subfields = $field->getFieldsForObject($object);
foreach ($subfields as $subkey => $subfield) {
if (isset($map[$subkey])) {
throw new Exception(
pht(
'Two HeraldFields (of classes "%s" and "%s") have the same '.
'field key ("%s") after expansion for an object of class '.
'"%s" inside adapter "%s". Each field must have a unique '.
'field key.',
get_class($subfield),
get_class($map[$subkey]),
$subkey,
get_class($object),
get_class($this)));
}
$subfield = id(clone $subfield)->setAdapter($this);
$map[$subkey] = $subfield;
}
}
$this->fieldMap = $map;
}
return $this->fieldMap;
}
private function getFieldImplementation($key) {
return idx($this->getFieldImplementationMap(), $key);
}
public function getFields() {
return array_keys($this->getFieldImplementationMap());
}
public function getFieldNameMap() {
return mpull($this->getFieldImplementationMap(), 'getHeraldFieldName');
}
public function getFieldGroupKey($field_key) {
$field = $this->getFieldImplementation($field_key);
if (!$field) {
return null;
}
return $field->getFieldGroupKey();
}
/* -( Conditions )--------------------------------------------------------- */
public function getConditionNameMap() {
return array(
self::CONDITION_CONTAINS => pht('contains'),
self::CONDITION_NOT_CONTAINS => pht('does not contain'),
self::CONDITION_GREATER => pht('is greater than'), //c4s custo
self::CONDITION_LESS => pht('is less than'), //c4s custo
self::CONDITION_IS => pht('is'),
self::CONDITION_IS_NOT => pht('is not'),
self::CONDITION_IS_ANY => pht('is any of'),
self::CONDITION_IS_TRUE => pht('is true'),
self::CONDITION_IS_FALSE => pht('is false'),
self::CONDITION_IS_NOT_ANY => pht('is not any of'),
self::CONDITION_INCLUDE_ALL => pht('include all of'),
self::CONDITION_INCLUDE_ANY => pht('include any of'),
self::CONDITION_INCLUDE_NONE => pht('do not include'),
self::CONDITION_IS_ME => pht('is myself'),
self::CONDITION_IS_NOT_ME => pht('is not myself'),
self::CONDITION_REGEXP => pht('matches regexp'),
self::CONDITION_NOT_REGEXP => pht('does not match regexp'),
self::CONDITION_RULE => pht('matches:'),
self::CONDITION_NOT_RULE => pht('does not match:'),
self::CONDITION_EXISTS => pht('exists'),
self::CONDITION_NOT_EXISTS => pht('does not exist'),
self::CONDITION_UNCONDITIONALLY => '', // don't show anything!
self::CONDITION_NEVER => '', // don't show anything!
self::CONDITION_REGEXP_PAIR => pht('matches regexp pair'),
self::CONDITION_HAS_BIT => pht('has bit'),
self::CONDITION_NOT_BIT => pht('lacks bit'),
);
}
public function getConditionsForField($field) {
return $this->requireFieldImplementation($field)
->getHeraldFieldConditions();
}
private function requireFieldImplementation($field_key) {
$field = $this->getFieldImplementation($field_key);
if (!$field) {
throw new Exception(
pht(
'No field with key "%s" is available to Herald adapter "%s".',
$field_key,
get_class($this)));
}
return $field;
}
public function doesConditionMatch(
HeraldEngine $engine,
HeraldRule $rule,
HeraldCondition $condition,
$field_value) {
$condition_type = $condition->getFieldCondition();
$condition_value = $condition->getValue();
switch ($condition_type) {
case self::CONDITION_CONTAINS:
case self::CONDITION_NOT_CONTAINS:
// "Contains and "does not contain" can take an array of strings, as in
// "Any changed filename" for diffs.
$result_if_match = ($condition_type == self::CONDITION_CONTAINS);
foreach ((array)$field_value as $value) {
if (stripos($value, $condition_value) !== false) {
return $result_if_match;
}
}
return !$result_if_match;
case self::CONDITION_GREATER: //c4s custo
return ($field_value > $condition_value);
case self::CONDITION_LESS: //c4s custo
return ($field_value < $condition_value);
case self::CONDITION_IS:
return ($field_value == $condition_value);
case self::CONDITION_IS_NOT:
return ($field_value != $condition_value);
case self::CONDITION_IS_ME:
return ($field_value == $rule->getAuthorPHID());
case self::CONDITION_IS_NOT_ME:
return ($field_value != $rule->getAuthorPHID());
case self::CONDITION_IS_ANY:
if (!is_array($condition_value)) {
throw new HeraldInvalidConditionException(
pht('Expected condition value to be an array.'));
}
$condition_value = array_fuse($condition_value);
return isset($condition_value[$field_value]);
case self::CONDITION_IS_NOT_ANY:
if (!is_array($condition_value)) {
throw new HeraldInvalidConditionException(
pht('Expected condition value to be an array.'));
}
$condition_value = array_fuse($condition_value);
return !isset($condition_value[$field_value]);
case self::CONDITION_INCLUDE_ALL:
if (!is_array($field_value)) {
throw new HeraldInvalidConditionException(
pht('Object produced non-array value!'));
}
if (!is_array($condition_value)) {
throw new HeraldInvalidConditionException(
pht('Expected condition value to be an array.'));
}
$have = array_select_keys(array_fuse($field_value), $condition_value);
return (count($have) == count($condition_value));
case self::CONDITION_INCLUDE_ANY:
return (bool)array_select_keys(
array_fuse($field_value),
$condition_value);
case self::CONDITION_INCLUDE_NONE:
return !array_select_keys(
array_fuse($field_value),
$condition_value);
case self::CONDITION_EXISTS:
case self::CONDITION_IS_TRUE:
return (bool)$field_value;
case self::CONDITION_NOT_EXISTS:
case self::CONDITION_IS_FALSE:
return !$field_value;
case self::CONDITION_UNCONDITIONALLY:
return (bool)$field_value;
case self::CONDITION_NEVER:
return false;
case self::CONDITION_REGEXP:
case self::CONDITION_NOT_REGEXP:
$result_if_match = ($condition_type == self::CONDITION_REGEXP);
foreach ((array)$field_value as $value) {
// We add the 'S' flag because we use the regexp multiple times.
// It shouldn't cause any troubles if the flag is already there
// - /.*/S is evaluated same as /.*/SS.
$result = @preg_match($condition_value.'S', $value);
if ($result === false) {
throw new HeraldInvalidConditionException(
pht(
'Regular expression "%s" in Herald rule "%s" is not valid, '.
'or exceeded backtracking or recursion limits while '.
'executing. Verify the expression and correct it or rewrite '.
'it with less backtracking.',
$condition_value,
$rule->getMonogram()));
}
if ($result) {
return $result_if_match;
}
}
return !$result_if_match;
case self::CONDITION_REGEXP_PAIR:
// Match a JSON-encoded pair of regular expressions against a
// dictionary. The first regexp must match the dictionary key, and the
// second regexp must match the dictionary value. If any key/value pair
// in the dictionary matches both regexps, the condition is satisfied.
$regexp_pair = null;
try {
$regexp_pair = phutil_json_decode($condition_value);
} catch (PhutilJSONParserException $ex) {
throw new HeraldInvalidConditionException(
pht('Regular expression pair is not valid JSON!'));
}
if (count($regexp_pair) != 2) {
throw new HeraldInvalidConditionException(
pht('Regular expression pair is not a pair!'));
}
$key_regexp = array_shift($regexp_pair);
$value_regexp = array_shift($regexp_pair);
foreach ((array)$field_value as $key => $value) {
$key_matches = @preg_match($key_regexp, $key);
if ($key_matches === false) {
throw new HeraldInvalidConditionException(
pht('First regular expression is invalid!'));
}
if ($key_matches) {
$value_matches = @preg_match($value_regexp, $value);
if ($value_matches === false) {
throw new HeraldInvalidConditionException(
pht('Second regular expression is invalid!'));
}
if ($value_matches) {
return true;
}
}
}
return false;
case self::CONDITION_RULE:
case self::CONDITION_NOT_RULE:
$rule = $engine->getRule($condition_value);
if (!$rule) {
throw new HeraldInvalidConditionException(
pht('Condition references a rule which does not exist!'));
}
$is_not = ($condition_type == self::CONDITION_NOT_RULE);
$result = $engine->doesRuleMatch($rule, $this);
if ($is_not) {
$result = !$result;
}
return $result;
case self::CONDITION_HAS_BIT:
return (($condition_value & $field_value) === (int)$condition_value);
case self::CONDITION_NOT_BIT:
return (($condition_value & $field_value) !== (int)$condition_value);
default:
throw new HeraldInvalidConditionException(
pht("Unknown condition '%s'.", $condition_type));
}
}
public function willSaveCondition(HeraldCondition $condition) {
$condition_type = $condition->getFieldCondition();
$condition_value = $condition->getValue();
switch ($condition_type) {
case self::CONDITION_REGEXP:
case self::CONDITION_NOT_REGEXP:
$ok = @preg_match($condition_value, '');
if ($ok === false) {
throw new HeraldInvalidConditionException(
pht(
'The regular expression "%s" is not valid. Regular expressions '.
'must have enclosing characters (e.g. "@/path/to/file@", not '.
'"/path/to/file") and be syntactically correct.',
$condition_value));
}
break;
case self::CONDITION_REGEXP_PAIR:
$json = null;
try {
$json = phutil_json_decode($condition_value);
} catch (PhutilJSONParserException $ex) {
throw new HeraldInvalidConditionException(
pht(
'The regular expression pair "%s" is not valid JSON. Enter a '.
'valid JSON array with two elements.',
$condition_value));
}
if (count($json) != 2) {
throw new HeraldInvalidConditionException(
pht(
'The regular expression pair "%s" must have exactly two '.
'elements.',
$condition_value));
}
$key_regexp = array_shift($json);
$val_regexp = array_shift($json);
$key_ok = @preg_match($key_regexp, '');
if ($key_ok === false) {
throw new HeraldInvalidConditionException(
pht(
'The first regexp in the regexp pair, "%s", is not a valid '.
'regexp.',
$key_regexp));
}
$val_ok = @preg_match($val_regexp, '');
if ($val_ok === false) {
throw new HeraldInvalidConditionException(
pht(
'The second regexp in the regexp pair, "%s", is not a valid '.
'regexp.',
$val_regexp));
}
break;
case self::CONDITION_CONTAINS:
case self::CONDITION_NOT_CONTAINS:
case self::CONDITION_GREATER: //c4s custo
case self::CONDITION_LESS: //c4s custo
case self::CONDITION_IS:
case self::CONDITION_IS_NOT:
case self::CONDITION_IS_ANY:
case self::CONDITION_IS_NOT_ANY:
case self::CONDITION_INCLUDE_ALL:
case self::CONDITION_INCLUDE_ANY:
case self::CONDITION_INCLUDE_NONE:
case self::CONDITION_IS_ME:
case self::CONDITION_IS_NOT_ME:
case self::CONDITION_RULE:
case self::CONDITION_NOT_RULE:
case self::CONDITION_EXISTS:
case self::CONDITION_NOT_EXISTS:
case self::CONDITION_UNCONDITIONALLY:
case self::CONDITION_NEVER:
case self::CONDITION_HAS_BIT:
case self::CONDITION_NOT_BIT:
case self::CONDITION_IS_TRUE:
case self::CONDITION_IS_FALSE:
// No explicit validation for these types, although there probably
// should be in some cases.
break;
default:
throw new HeraldInvalidConditionException(
pht(
'Unknown condition "%s"!',
$condition_type));
}
}
/* -( Actions )------------------------------------------------------------ */
private function getActionImplementationMap() {
if ($this->actionMap === null) {
// We can't use PhutilClassMapQuery here because action expansion
// depends on the adapter and object.
$object = $this->getObject();
$map = array();
$all = HeraldAction::getAllActions();
foreach ($all as $key => $action) {
$action = id(clone $action)->setAdapter($this);
if (!$action->supportsObject($object)) {
continue;
}
$subactions = $action->getActionsForObject($object);
foreach ($subactions as $subkey => $subaction) {
if (isset($map[$subkey])) {
throw new Exception(
pht(
'Two HeraldActions (of classes "%s" and "%s") have the same '.
'action key ("%s") after expansion for an object of class '.
'"%s" inside adapter "%s". Each action must have a unique '.
'action key.',
get_class($subaction),
get_class($map[$subkey]),
$subkey,
get_class($object),
get_class($this)));
}
$subaction = id(clone $subaction)->setAdapter($this);
$map[$subkey] = $subaction;
}
}
$this->actionMap = $map;
}
return $this->actionMap;
}
private function requireActionImplementation($action_key) {
$action = $this->getActionImplementation($action_key);
if (!$action) {
throw new Exception(
pht(
'No action with key "%s" is available to Herald adapter "%s".',
$action_key,
get_class($this)));
}
return $action;
}
private function getActionsForRuleType($rule_type) {
$actions = $this->getActionImplementationMap();
foreach ($actions as $key => $action) {
if (!$action->supportsRuleType($rule_type)) {
unset($actions[$key]);
}
}
return $actions;
}
public function getActionImplementation($key) {
return idx($this->getActionImplementationMap(), $key);
}
public function getActionKeys() {
return array_keys($this->getActionImplementationMap());
}
public function getActionGroupKey($action_key) {
$action = $this->getActionImplementation($action_key);
if (!$action) {
return null;
}
return $action->getActionGroupKey();
}
public function getActions($rule_type) {
$actions = array();
foreach ($this->getActionsForRuleType($rule_type) as $key => $action) {
$actions[] = $key;
}
return $actions;
}
public function getActionNameMap($rule_type) {
$map = array();
foreach ($this->getActionsForRuleType($rule_type) as $key => $action) {
$map[$key] = $action->getHeraldActionName();
}
return $map;
}
public function willSaveAction(
HeraldRule $rule,
HeraldActionRecord $action) {
$impl = $this->requireActionImplementation($action->getAction());
$target = $action->getTarget();
$target = $impl->willSaveActionValue($target);
$action->setTarget($target);
}
/* -( Values )------------------------------------------------------------- */
public function getValueTypeForFieldAndCondition($field, $condition) {
return $this->requireFieldImplementation($field)
->getHeraldFieldValueType($condition);
}
public function getValueTypeForAction($action, $rule_type) {
$impl = $this->requireActionImplementation($action);
return $impl->getHeraldActionValueType();
}
private function buildTokenizerFieldValue(
PhabricatorTypeaheadDatasource $datasource) {
$key = 'action.'.get_class($datasource);
return id(new HeraldTokenizerFieldValue())
->setKey($key)
->setDatasource($datasource);
}
/* -( Repetition )--------------------------------------------------------- */
public function getRepetitionOptions() {
$options = array();
$options[] = HeraldRule::REPEAT_EVERY;
// Some rules, like pre-commit rules, only ever fire once. It doesn't
// make sense to use state-based repetition policies like "only the first
// time" for these rules.
if (!$this->isSingleEventAdapter()) {
$options[] = HeraldRule::REPEAT_FIRST;
$options[] = HeraldRule::REPEAT_CHANGE;
}
return $options;
}
protected function initializeNewAdapter() {
$this->setObject($this->newObject());
return $this;
}
/**
* Does this adapter's event fire only once?
*
* Single use adapters (like pre-commit and diff adapters) only fire once,
* so fields like "Is new object" don't make sense to apply to their content.
*
* @return bool
*/
public function isSingleEventAdapter() {
return false;
}
public static function getAllAdapters() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getAdapterContentType')
->setSortMethod('getAdapterSortKey')
->execute();
}
public static function getAdapterForContentType($content_type) {
$adapters = self::getAllAdapters();
foreach ($adapters as $adapter) {
if ($adapter->getAdapterContentType() == $content_type) {
$adapter = id(clone $adapter);
$adapter->initializeNewAdapter();
return $adapter;
}
}
throw new Exception(
pht(
'No adapter exists for Herald content type "%s".',
$content_type));
}
public static function getEnabledAdapterMap(PhabricatorUser $viewer) {
$map = array();
$adapters = self::getAllAdapters();
foreach ($adapters as $adapter) {
if (!$adapter->isAvailableToUser($viewer)) {
continue;
}
$type = $adapter->getAdapterContentType();
$name = $adapter->getAdapterContentName();
$map[$type] = $name;
}
return $map;
}
public function getEditorValueForCondition(
PhabricatorUser $viewer,
HeraldCondition $condition) {
$field = $this->requireFieldImplementation($condition->getFieldName());
return $field->getEditorValue(
$viewer,
$condition->getFieldCondition(),
$condition->getValue());
}
public function getEditorValueForAction(
PhabricatorUser $viewer,
HeraldActionRecord $action_record) {
$action = $this->requireActionImplementation($action_record->getAction());
return $action->getEditorValue(
$viewer,
$action_record->getTarget());
}
public function renderRuleAsText(
HeraldRule $rule,
PhabricatorHandleList $handles,
PhabricatorUser $viewer) {
require_celerity_resource('herald-css');
$icon = id(new PHUIIconView())
->setIcon('fa-chevron-circle-right lightgreytext')
->addClass('herald-list-icon');
if ($rule->getMustMatchAll()) {
$match_text = pht('When all of these conditions are met:');
} else {
$match_text = pht('When any of these conditions are met:');
}
$match_title = phutil_tag(
'p',
array(
'class' => 'herald-list-description',
),
$match_text);
$match_list = array();
foreach ($rule->getConditions() as $condition) {
$match_list[] = phutil_tag(
'div',
array(
'class' => 'herald-list-item',
),
array(
$icon,
$this->renderConditionAsText($condition, $handles, $viewer),
));
}
if ($rule->isRepeatFirst()) {
$action_text = pht(
'Take these actions the first time this rule matches:');
} else if ($rule->isRepeatOnChange()) {
$action_text = pht(
'Take these actions if this rule did not match the last time:');
} else {
$action_text = pht(
'Take these actions every time this rule matches:');
}
$action_title = phutil_tag(
'p',
array(
'class' => 'herald-list-description',
),
$action_text);
$action_list = array();
foreach ($rule->getActions() as $action) {
$action_list[] = phutil_tag(
'div',
array(
'class' => 'herald-list-item',
),
array(
$icon,
$this->renderActionAsText($viewer, $action, $handles),
));
}
return array(
$match_title,
$match_list,
$action_title,
$action_list,
);
}
private function renderConditionAsText(
HeraldCondition $condition,
PhabricatorHandleList $handles,
PhabricatorUser $viewer) {
$field_type = $condition->getFieldName();
$field = $this->getFieldImplementation($field_type);
if (!$field) {
return pht('Unknown Field: "%s"', $field_type);
}
$field_name = $field->getHeraldFieldName();
$condition_type = $condition->getFieldCondition();
$condition_name = idx($this->getConditionNameMap(), $condition_type);
$value = $this->renderConditionValueAsText($condition, $handles, $viewer);
return array(
$field_name,
' ',
$condition_name,
' ',
$value,
);
}
private function renderActionAsText(
PhabricatorUser $viewer,
HeraldActionRecord $action,
PhabricatorHandleList $handles) {
$impl = $this->getActionImplementation($action->getAction());
if ($impl) {
$impl->setViewer($viewer);
$value = $action->getTarget();
return $impl->renderActionDescription($value);
}
$rule_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL;
$action_type = $action->getAction();
$default = pht('(Unknown Action "%s") equals', $action_type);
$action_name = idx(
$this->getActionNameMap($rule_global),
$action_type,
$default);
$target = $this->renderActionTargetAsText($action, $handles);
return hsprintf(' %s %s', $action_name, $target);
}
private function renderConditionValueAsText(
HeraldCondition $condition,
PhabricatorHandleList $handles,
PhabricatorUser $viewer) {
$field = $this->requireFieldImplementation($condition->getFieldName());
return $field->renderConditionValue(
$viewer,
$condition->getFieldCondition(),
$condition->getValue());
}
private function renderActionTargetAsText(
HeraldActionRecord $action,
PhabricatorHandleList $handles) {
// TODO: This should be driven through HeraldAction.
$target = $action->getTarget();
if (!is_array($target)) {
$target = array($target);
}
foreach ($target as $index => $val) {
switch ($action->getAction()) {
default:
$handle = $handles->getHandleIfExists($val);
if ($handle) {
$target[$index] = $handle->renderLink();
}
break;
}
}
$target = phutil_implode_html(', ', $target);
return $target;
}
/**
* Given a @{class:HeraldRule}, this function extracts all the phids that
* we'll want to load as handles later.
*
* This function performs a somewhat hacky approach to figuring out what
* is and is not a phid - try to get the phid type and if the type is
* *not* unknown assume its a valid phid.
*
* Don't try this at home. Use more strongly typed data at home.
*
* Think of the children.
*/
public static function getHandlePHIDs(HeraldRule $rule) {
$phids = array($rule->getAuthorPHID());
foreach ($rule->getConditions() as $condition) {
$value = $condition->getValue();
if (!is_array($value)) {
$value = array($value);
}
foreach ($value as $val) {
if (phid_get_type($val) !=
PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
$phids[] = $val;
}
}
}
foreach ($rule->getActions() as $action) {
$target = $action->getTarget();
if (!is_array($target)) {
$target = array($target);
}
foreach ($target as $val) {
if (phid_get_type($val) !=
PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
$phids[] = $val;
}
}
}
if ($rule->isObjectRule()) {
$phids[] = $rule->getTriggerObjectPHID();
}
return $phids;
}
/* -( Applying Effects )--------------------------------------------------- */
/**
* @task apply
*/
protected function applyStandardEffect(HeraldEffect $effect) {
$action = $effect->getAction();
$rule_type = $effect->getRule()->getRuleType();
$impl = $this->getActionImplementation($action);
if (!$impl) {
return new HeraldApplyTranscript(
$effect,
false,
array(
array(
HeraldAction::DO_STANDARD_INVALID_ACTION,
$action,
),
));
}
if (!$impl->supportsRuleType($rule_type)) {
return new HeraldApplyTranscript(
$effect,
false,
array(
array(
HeraldAction::DO_STANDARD_WRONG_RULE_TYPE,
$rule_type,
),
));
}
$impl->applyEffect($this->getObject(), $effect);
return $impl->getApplyTranscript($effect);
}
public function loadEdgePHIDs($type) {
if (!isset($this->edgeCache[$type])) {
$phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getObject()->getPHID(),
$type);
$this->edgeCache[$type] = array_fuse($phids);
}
return $this->edgeCache[$type];
}
/* -( Forbidden Actions )-------------------------------------------------- */
final public function getForbiddenActions() {
return array_keys($this->forbiddenActions);
}
final public function setForbiddenAction($action, $reason) {
$this->forbiddenActions[$action] = $reason;
return $this;
}
final public function getRequiredFieldStates($field_key) {
return $this->requireFieldImplementation($field_key)
->getRequiredAdapterStates();
}
final public function getRequiredActionStates($action_key) {
return $this->requireActionImplementation($action_key)
->getRequiredAdapterStates();
}
final public function getForbiddenReason($action) {
if (!isset($this->forbiddenActions[$action])) {
throw new Exception(
pht(
'Action "%s" is not forbidden!',
$action));
}
return $this->forbiddenActions[$action];
}
/* -( Must Encrypt )------------------------------------------------------- */
final public function addMustEncryptReason($reason) {
$this->mustEncryptReasons[] = $reason;
return $this;
}
final public function getMustEncryptReasons() {
return $this->mustEncryptReasons;
}
/* -( Webhooks )----------------------------------------------------------- */
public function supportsWebhooks() {
return true;
}
final public function queueWebhook($webhook_phid, $rule_phid) {
$this->webhookMap[$webhook_phid][] = $rule_phid;
return $this;
}
final public function getWebhookMap() {
return $this->webhookMap;
}
}
diff --git a/src/applications/herald/controller/HeraldDisableController.php b/src/applications/herald/controller/HeraldDisableController.php
index def87049f..765237930 100644
--- a/src/applications/herald/controller/HeraldDisableController.php
+++ b/src/applications/herald/controller/HeraldDisableController.php
@@ -1,66 +1,66 @@
<?php
final class HeraldDisableController extends HeraldController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$action = $request->getURIData('action');
$rule = id(new HeraldRuleQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$rule) {
return new Aphront404Response();
}
if ($rule->isGlobalRule()) {
$this->requireApplicationCapability(
HeraldManageGlobalRulesCapability::CAPABILITY);
}
$view_uri = '/'.$rule->getMonogram();
$is_disable = ($action === 'disable');
if ($request->isFormPost()) {
$xaction = id(new HeraldRuleTransaction())
- ->setTransactionType(HeraldRuleTransaction::TYPE_DISABLE)
+ ->setTransactionType(HeraldRuleDisableTransaction::TRANSACTIONTYPE)
->setNewValue($is_disable);
id(new HeraldRuleEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request)
->applyTransactions($rule, array($xaction));
return id(new AphrontRedirectResponse())->setURI($view_uri);
}
if ($is_disable) {
$title = pht('Really disable this rule?');
$body = pht('This rule will no longer activate.');
$button = pht('Disable Rule');
} else {
$title = pht('Really enable this rule?');
$body = pht('This rule will become active again.');
$button = pht('Enable Rule');
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle($title)
->appendChild($body)
->addSubmitButton($button)
->addCancelButton($view_uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/herald/controller/HeraldNewController.php b/src/applications/herald/controller/HeraldNewController.php
index fbaf1aeb9..f571aeb39 100644
--- a/src/applications/herald/controller/HeraldNewController.php
+++ b/src/applications/herald/controller/HeraldNewController.php
@@ -1,316 +1,316 @@
<?php
final class HeraldNewController extends HeraldController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer);
$rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap();
$errors = array();
$e_type = null;
$e_rule = null;
$e_object = null;
$step = $request->getInt('step');
if ($request->isFormPost()) {
$content_type = $request->getStr('content_type');
if (empty($content_type_map[$content_type])) {
$errors[] = pht('You must choose a content type for this rule.');
$e_type = pht('Required');
$step = 0;
}
if (!$errors && $step > 1) {
$rule_type = $request->getStr('rule_type');
if (empty($rule_type_map[$rule_type])) {
$errors[] = pht('You must choose a rule type for this rule.');
$e_rule = pht('Required');
$step = 1;
}
}
if (!$errors && $step >= 2) {
$target_phid = null;
$object_name = $request->getStr('objectName');
$done = false;
if ($rule_type != HeraldRuleTypeConfig::RULE_TYPE_OBJECT) {
$done = true;
} else if (strlen($object_name)) {
$target_object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withNames(array($object_name))
->executeOne();
if ($target_object) {
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$target_object,
PhabricatorPolicyCapability::CAN_EDIT);
if (!$can_edit) {
$errors[] = pht(
'You can not create a rule for that object, because you do '.
'not have permission to edit it. You can only create rules '.
'for objects you can edit.');
$e_object = pht('Not Editable');
$step = 2;
} else {
$adapter = HeraldAdapter::getAdapterForContentType($content_type);
if (!$adapter->canTriggerOnObject($target_object)) {
$errors[] = pht(
'This object is not of an allowed type for the rule. '.
'Rules can only trigger on certain objects.');
$e_object = pht('Invalid');
$step = 2;
} else {
$target_phid = $target_object->getPHID();
$done = true;
}
}
} else {
$errors[] = pht('No object exists by that name.');
$e_object = pht('Invalid');
$step = 2;
}
} else if ($step > 2) {
$errors[] = pht(
'You must choose an object to associate this rule with.');
$e_object = pht('Required');
$step = 2;
}
if (!$errors && $done) {
- $uri = id(new PhutilURI('edit/'))
- ->setQueryParams(
- array(
- 'content_type' => $content_type,
- 'rule_type' => $rule_type,
- 'targetPHID' => $target_phid,
- ));
+ $params = array(
+ 'content_type' => $content_type,
+ 'rule_type' => $rule_type,
+ 'targetPHID' => $target_phid,
+ );
+
+ $uri = new PhutilURI('edit/', $params);
$uri = $this->getApplicationURI($uri);
return id(new AphrontRedirectResponse())->setURI($uri);
}
}
}
$content_type = $request->getStr('content_type');
$rule_type = $request->getStr('rule_type');
$form = id(new AphrontFormView())
->setUser($viewer)
->setAction($this->getApplicationURI('new/'));
switch ($step) {
case 0:
default:
$content_types = $this->renderContentTypeControl(
$content_type_map,
$e_type);
$form
->addHiddenInput('step', 1)
->appendChild($content_types);
$cancel_text = null;
$cancel_uri = $this->getApplicationURI();
$title = pht('Create Herald Rule');
break;
case 1:
$rule_types = $this->renderRuleTypeControl(
$rule_type_map,
$e_rule);
$form
->addHiddenInput('content_type', $content_type)
->addHiddenInput('step', 2)
->appendChild($rule_types);
+ $params = array(
+ 'content_type' => $content_type,
+ 'step' => '0',
+ );
+
$cancel_text = pht('Back');
- $cancel_uri = id(new PhutilURI('new/'))
- ->setQueryParams(
- array(
- 'content_type' => $content_type,
- 'step' => 0,
- ));
+ $cancel_uri = new PhutilURI('new/', $params);
$cancel_uri = $this->getApplicationURI($cancel_uri);
$title = pht('Create Herald Rule: %s',
idx($content_type_map, $content_type));
break;
case 2:
$adapter = HeraldAdapter::getAdapterForContentType($content_type);
$form
->addHiddenInput('content_type', $content_type)
->addHiddenInput('rule_type', $rule_type)
->addHiddenInput('step', 3)
->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Rule for'))
->setValue(
phutil_tag(
'strong',
array(),
idx($content_type_map, $content_type))))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Rule Type'))
->setValue(
phutil_tag(
'strong',
array(),
idx($rule_type_map, $rule_type))))
->appendRemarkupInstructions(
pht(
'Choose the object this rule will act on (for example, enter '.
'`rX` to act on the `rX` repository, or `#project` to act on '.
'a project).'))
->appendRemarkupInstructions(
$adapter->explainValidTriggerObjects())
->appendChild(
id(new AphrontFormTextControl())
->setName('objectName')
->setError($e_object)
->setValue($request->getStr('objectName'))
->setLabel(pht('Object')));
+ $params = array(
+ 'content_type' => $content_type,
+ 'rule_type' => $rule_type,
+ 'step' => 1,
+ );
+
$cancel_text = pht('Back');
- $cancel_uri = id(new PhutilURI('new/'))
- ->setQueryParams(
- array(
- 'content_type' => $content_type,
- 'rule_type' => $rule_type,
- 'step' => 1,
- ));
+ $cancel_uri = new PhutilURI('new/', $params);
$cancel_uri = $this->getApplicationURI($cancel_uri);
$title = pht('Create Herald Rule: %s',
idx($content_type_map, $content_type));
break;
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Continue'))
->addCancelButton($cancel_uri, $cancel_text));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setFormErrors($errors)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->setForm($form);
$crumbs = $this
->buildApplicationCrumbs()
->addTextCrumb(pht('Create Rule'))
->setBorder(true);
$view = id(new PHUITwoColumnView())
->setFooter($form_box);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild(
array(
$view,
));
}
private function renderContentTypeControl(array $content_type_map, $e_type) {
$request = $this->getRequest();
$radio = id(new AphrontFormRadioButtonControl())
->setLabel(pht('New Rule for'))
->setName('content_type')
->setValue($request->getStr('content_type'))
->setError($e_type);
foreach ($content_type_map as $value => $name) {
$adapter = HeraldAdapter::getAdapterForContentType($value);
$radio->addButton(
$value,
$name,
phutil_escape_html_newlines($adapter->getAdapterContentDescription()));
}
return $radio;
}
private function renderRuleTypeControl(array $rule_type_map, $e_rule) {
$request = $this->getRequest();
// Reorder array to put less powerful rules first.
$rule_type_map = array_select_keys(
$rule_type_map,
array(
HeraldRuleTypeConfig::RULE_TYPE_PERSONAL,
HeraldRuleTypeConfig::RULE_TYPE_OBJECT,
HeraldRuleTypeConfig::RULE_TYPE_GLOBAL,
)) + $rule_type_map;
list($can_global, $global_link) = $this->explainApplicationCapability(
HeraldManageGlobalRulesCapability::CAPABILITY,
pht('You have permission to create and manage global rules.'),
pht('You do not have permission to create or manage global rules.'));
$captions = array(
HeraldRuleTypeConfig::RULE_TYPE_PERSONAL =>
pht(
'Personal rules notify you about events. You own them, but they can '.
'only affect you. Personal rules only trigger for objects you have '.
'permission to see.'),
HeraldRuleTypeConfig::RULE_TYPE_OBJECT =>
pht(
'Object rules notify anyone about events. They are bound to an '.
'object (like a repository) and can only act on that object. You '.
'must be able to edit an object to create object rules for it. '.
'Other users who can edit the object can edit its rules.'),
HeraldRuleTypeConfig::RULE_TYPE_GLOBAL =>
array(
pht(
'Global rules notify anyone about events. Global rules can '.
'bypass access control policies and act on any object.'),
$global_link,
),
);
$radio = id(new AphrontFormRadioButtonControl())
->setLabel(pht('Rule Type'))
->setName('rule_type')
->setValue($request->getStr('rule_type'))
->setError($e_rule);
$adapter = HeraldAdapter::getAdapterForContentType(
$request->getStr('content_type'));
foreach ($rule_type_map as $value => $name) {
$caption = idx($captions, $value);
$disabled = ($value == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) &&
(!$can_global);
if (!$adapter->supportsRuleType($value)) {
$disabled = true;
$caption = array(
$caption,
"\n\n",
phutil_tag(
'em',
array(),
pht(
'This rule type is not supported by the selected content type.')),
);
}
$radio->addButton(
$value,
$name,
phutil_escape_html_newlines($caption),
$disabled ? 'disabled' : null,
$disabled);
}
return $radio;
}
}
diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php
index d400f8ae9..d05ed2d52 100644
--- a/src/applications/herald/controller/HeraldRuleController.php
+++ b/src/applications/herald/controller/HeraldRuleController.php
@@ -1,733 +1,743 @@
<?php
final class HeraldRuleController extends HeraldController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer);
$rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap();
if ($id) {
$rule = id(new HeraldRuleQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$rule) {
return new Aphront404Response();
}
$cancel_uri = '/'.$rule->getMonogram();
} else {
$new_uri = $this->getApplicationURI('new/');
$rule = new HeraldRule();
$rule->setAuthorPHID($viewer->getPHID());
$rule->setMustMatchAll(1);
$content_type = $request->getStr('content_type');
$rule->setContentType($content_type);
$rule_type = $request->getStr('rule_type');
if (!isset($rule_type_map[$rule_type])) {
return $this->newDialog()
->setTitle(pht('Invalid Rule Type'))
->appendParagraph(
pht(
'The selected rule type ("%s") is not recognized by Herald.',
$rule_type))
->addCancelButton($new_uri);
}
$rule->setRuleType($rule_type);
try {
$adapter = HeraldAdapter::getAdapterForContentType(
$rule->getContentType());
} catch (Exception $ex) {
return $this->newDialog()
->setTitle(pht('Invalid Content Type'))
->appendParagraph(
pht(
'The selected content type ("%s") is not recognized by '.
'Herald.',
$rule->getContentType()))
->addCancelButton($new_uri);
}
if (!$adapter->supportsRuleType($rule->getRuleType())) {
return $this->newDialog()
->setTitle(pht('Rule/Content Mismatch'))
->appendParagraph(
pht(
'The selected rule type ("%s") is not supported by the selected '.
'content type ("%s").',
$rule->getRuleType(),
$rule->getContentType()))
->addCancelButton($new_uri);
}
if ($rule->isObjectRule()) {
$rule->setTriggerObjectPHID($request->getStr('targetPHID'));
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($rule->getTriggerObjectPHID()))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$object) {
throw new Exception(
pht('No valid object provided for object rule!'));
}
if (!$adapter->canTriggerOnObject($object)) {
throw new Exception(
pht('Object is of wrong type for adapter!'));
}
}
$cancel_uri = $this->getApplicationURI();
}
if ($rule->isGlobalRule()) {
$this->requireApplicationCapability(
HeraldManageGlobalRulesCapability::CAPABILITY);
}
$adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType());
$local_version = id(new HeraldRule())->getConfigVersion();
if ($rule->getConfigVersion() > $local_version) {
throw new Exception(
pht(
'This rule was created with a newer version of Herald. You can not '.
'view or edit it in this older version. Upgrade your Phabricator '.
'deployment.'));
}
// Upgrade rule version to our version, since we might add newly-defined
// conditions, etc.
$rule->setConfigVersion($local_version);
$rule_conditions = $rule->loadConditions();
$rule_actions = $rule->loadActions();
$rule->attachConditions($rule_conditions);
$rule->attachActions($rule_actions);
$e_name = true;
$errors = array();
if ($request->isFormPost() && $request->getStr('save')) {
list($e_name, $errors) = $this->saveRule($adapter, $rule, $request);
if (!$errors) {
$id = $rule->getID();
$uri = '/'.$rule->getMonogram();
return id(new AphrontRedirectResponse())->setURI($uri);
}
}
$must_match_selector = $this->renderMustMatchSelector($rule);
$repetition_selector = $this->renderRepetitionSelector($rule, $adapter);
$handles = $this->loadHandlesForRule($rule);
require_celerity_resource('herald-css');
$content_type_name = $content_type_map[$rule->getContentType()];
$rule_type_name = $rule_type_map[$rule->getRuleType()];
$form = id(new AphrontFormView())
->setUser($viewer)
->setID('herald-rule-edit-form')
->addHiddenInput('content_type', $rule->getContentType())
->addHiddenInput('rule_type', $rule->getRuleType())
->addHiddenInput('save', 1)
->appendChild(
// Build this explicitly (instead of using addHiddenInput())
// so we can add a sigil to it.
javelin_tag(
'input',
array(
'type' => 'hidden',
'name' => 'rule',
'sigil' => 'rule',
)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Rule Name'))
->setName('name')
->setError($e_name)
->setValue($rule->getName()));
$trigger_object_control = false;
if ($rule->isObjectRule()) {
$trigger_object_control = id(new AphrontFormStaticControl())
->setValue(
pht(
'This rule triggers for %s.',
$handles[$rule->getTriggerObjectPHID()]->renderLink()));
}
$form
->appendChild(
id(new AphrontFormMarkupControl())
->setValue(pht(
'This %s rule triggers for %s.',
phutil_tag('strong', array(), $rule_type_name),
phutil_tag('strong', array(), $content_type_name))))
->appendChild($trigger_object_control)
->appendChild(
id(new PHUIFormInsetView())
->setTitle(pht('Conditions'))
->setRightButton(javelin_tag(
'a',
array(
'href' => '#',
'class' => 'button button-green',
'sigil' => 'create-condition',
'mustcapture' => true,
),
pht('New Condition')))
->setDescription(
pht('When %s these conditions are met:', $must_match_selector))
->setContent(javelin_tag(
'table',
array(
'sigil' => 'rule-conditions',
'class' => 'herald-condition-table',
),
'')))
->appendChild(
id(new PHUIFormInsetView())
->setTitle(pht('Action'))
->setRightButton(javelin_tag(
'a',
array(
'href' => '#',
'class' => 'button button-green',
'sigil' => 'create-action',
'mustcapture' => true,
),
pht('New Action')))
->setDescription(pht(
'Take these actions %s',
$repetition_selector))
->setContent(javelin_tag(
'table',
array(
'sigil' => 'rule-actions',
'class' => 'herald-action-table',
),
'')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save Rule'))
->addCancelButton($cancel_uri));
$this->setupEditorBehavior($rule, $handles, $adapter);
$title = $rule->getID()
? pht('Edit Herald Rule: %s', $rule->getName())
: pht('Create Herald Rule: %s', idx($content_type_map, $content_type));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->setFormErrors($errors)
->setForm($form);
$crumbs = $this
->buildApplicationCrumbs()
->addTextCrumb($title)
->setBorder(true);
$view = id(new PHUITwoColumnView())
->setFooter($form_box);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild(
array(
$view,
));
}
private function saveRule(HeraldAdapter $adapter, $rule, $request) {
$new_name = $request->getStr('name');
$match_all = ($request->getStr('must_match') == 'all');
$repetition_policy = $request->getStr('repetition_policy');
// If the user selected an invalid policy, or there's only one possible
// value so we didn't render a control, adjust the value to the first
// valid policy value.
$repetition_options = $this->getRepetitionOptionMap($adapter);
if (!isset($repetition_options[$repetition_policy])) {
$repetition_policy = head_key($repetition_options);
}
$e_name = true;
$errors = array();
if (!strlen($new_name)) {
$e_name = pht('Required');
$errors[] = pht('Rule must have a name.');
}
$data = null;
try {
$data = phutil_json_decode($request->getStr('rule'));
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht('Failed to decode rule data.'),
$ex);
}
if (!is_array($data) ||
!$data['conditions'] ||
!$data['actions']) {
throw new Exception(pht('Failed to decode rule data.'));
}
$conditions = array();
foreach ($data['conditions'] as $condition) {
if ($condition === null) {
// We manage this as a sparse array on the client, so may receive
// NULL if conditions have been removed.
continue;
}
$obj = new HeraldCondition();
$obj->setFieldName($condition[0]);
$obj->setFieldCondition($condition[1]);
if (is_array($condition[2])) {
$obj->setValue(array_keys($condition[2]));
} else {
$obj->setValue($condition[2]);
}
try {
$adapter->willSaveCondition($obj);
} catch (HeraldInvalidConditionException $ex) {
$errors[] = $ex->getMessage();
}
$conditions[] = $obj;
}
$actions = array();
foreach ($data['actions'] as $action) {
if ($action === null) {
// Sparse on the client; removals can give us NULLs.
continue;
}
if (!isset($action[1])) {
// Legitimate for any action which doesn't need a target, like
// "Do nothing".
$action[1] = null;
}
$obj = new HeraldActionRecord();
$obj->setAction($action[0]);
$obj->setTarget($action[1]);
try {
$adapter->willSaveAction($rule, $obj);
} catch (HeraldInvalidActionException $ex) {
$errors[] = $ex->getMessage();
}
$actions[] = $obj;
}
if (!$errors) {
$new_state = id(new HeraldRuleSerializer())->serializeRuleComponents(
$match_all,
$conditions,
$actions,
$repetition_policy);
$xactions = array();
+
+ // Until this moves to EditEngine, manually add a "CREATE" transaction
+ // if we're creating a new rule. This improves rendering of the initial
+ // group of transactions.
+ $is_new = (bool)(!$rule->getID());
+ if ($is_new) {
+ $xactions[] = id(new HeraldRuleTransaction())
+ ->setTransactionType(PhabricatorTransactions::TYPE_CREATE);
+ }
+
$xactions[] = id(new HeraldRuleTransaction())
- ->setTransactionType(HeraldRuleTransaction::TYPE_EDIT)
+ ->setTransactionType(HeraldRuleEditTransaction::TRANSACTIONTYPE)
->setNewValue($new_state);
$xactions[] = id(new HeraldRuleTransaction())
- ->setTransactionType(HeraldRuleTransaction::TYPE_NAME)
+ ->setTransactionType(HeraldRuleNameTransaction::TRANSACTIONTYPE)
->setNewValue($new_name);
try {
id(new HeraldRuleEditor())
->setActor($this->getViewer())
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request)
->applyTransactions($rule, $xactions);
return array(null, null);
} catch (Exception $ex) {
$errors[] = $ex->getMessage();
}
}
// mutate current rule, so it would be sent to the client in the right state
$rule->setMustMatchAll((int)$match_all);
$rule->setName($new_name);
$rule->setRepetitionPolicyStringConstant($repetition_policy);
$rule->attachConditions($conditions);
$rule->attachActions($actions);
return array($e_name, $errors);
}
private function setupEditorBehavior(
HeraldRule $rule,
array $handles,
HeraldAdapter $adapter) {
$all_rules = $this->loadRulesThisRuleMayDependUpon($rule);
$all_rules = mpull($all_rules, 'getName', 'getPHID');
asort($all_rules);
$all_fields = $adapter->getFieldNameMap();
$all_conditions = $adapter->getConditionNameMap();
$all_actions = $adapter->getActionNameMap($rule->getRuleType());
$fields = $adapter->getFields();
$field_map = array_select_keys($all_fields, $fields);
// Populate any fields which exist in the rule but which we don't know the
// names of, so that saving a rule without touching anything doesn't change
// it.
foreach ($rule->getConditions() as $condition) {
$field_name = $condition->getFieldName();
if (empty($field_map[$field_name])) {
$field_map[$field_name] = pht('<Unknown Field "%s">', $field_name);
}
}
$actions = $adapter->getActions($rule->getRuleType());
$action_map = array_select_keys($all_actions, $actions);
// Populate any actions which exist in the rule but which we don't know the
// names of, so that saving a rule without touching anything doesn't change
// it.
foreach ($rule->getActions() as $action) {
$action_name = $action->getAction();
if (empty($action_map[$action_name])) {
$action_map[$action_name] = pht('<Unknown Action "%s">', $action_name);
}
}
$config_info = array();
$config_info['fields'] = $this->getFieldGroups($adapter, $field_map);
$config_info['conditions'] = $all_conditions;
$config_info['actions'] = $this->getActionGroups($adapter, $action_map);
$config_info['valueMap'] = array();
foreach ($field_map as $field => $name) {
try {
$field_conditions = $adapter->getConditionsForField($field);
} catch (Exception $ex) {
$field_conditions = array(HeraldAdapter::CONDITION_UNCONDITIONALLY);
}
$config_info['conditionMap'][$field] = $field_conditions;
}
foreach ($field_map as $field => $fname) {
foreach ($config_info['conditionMap'][$field] as $condition) {
$value_key = $adapter->getValueTypeForFieldAndCondition(
$field,
$condition);
if ($value_key instanceof HeraldFieldValue) {
$value_key->setViewer($this->getViewer());
$spec = $value_key->getControlSpecificationDictionary();
$value_key = $value_key->getFieldValueKey();
$config_info['valueMap'][$value_key] = $spec;
}
$config_info['values'][$field][$condition] = $value_key;
}
}
$config_info['rule_type'] = $rule->getRuleType();
foreach ($action_map as $action => $name) {
try {
$value_key = $adapter->getValueTypeForAction(
$action,
$rule->getRuleType());
} catch (Exception $ex) {
$value_key = new HeraldEmptyFieldValue();
}
if ($value_key instanceof HeraldFieldValue) {
$value_key->setViewer($this->getViewer());
$spec = $value_key->getControlSpecificationDictionary();
$value_key = $value_key->getFieldValueKey();
$config_info['valueMap'][$value_key] = $spec;
}
$config_info['targets'][$action] = $value_key;
}
$default_group = head($config_info['fields']);
$default_field = head_key($default_group['options']);
$default_condition = head($config_info['conditionMap'][$default_field]);
$default_actions = head($config_info['actions']);
$default_action = head_key($default_actions['options']);
if ($rule->getConditions()) {
$serial_conditions = array();
foreach ($rule->getConditions() as $condition) {
$value = $adapter->getEditorValueForCondition(
$this->getViewer(),
$condition);
$serial_conditions[] = array(
$condition->getFieldName(),
$condition->getFieldCondition(),
$value,
);
}
} else {
$serial_conditions = array(
array($default_field, $default_condition, null),
);
}
if ($rule->getActions()) {
$serial_actions = array();
foreach ($rule->getActions() as $action) {
$value = $adapter->getEditorValueForAction(
$this->getViewer(),
$action);
$serial_actions[] = array(
$action->getAction(),
$value,
);
}
} else {
$serial_actions = array(
array($default_action, null),
);
}
Javelin::initBehavior(
'herald-rule-editor',
array(
'root' => 'herald-rule-edit-form',
'default' => array(
'field' => $default_field,
'condition' => $default_condition,
'action' => $default_action,
),
'conditions' => (object)$serial_conditions,
'actions' => (object)$serial_actions,
'template' => $this->buildTokenizerTemplates() + array(
'rules' => $all_rules,
),
'info' => $config_info,
));
}
private function loadHandlesForRule($rule) {
$phids = array();
foreach ($rule->getActions() as $action) {
if (!is_array($action->getTarget())) {
continue;
}
foreach ($action->getTarget() as $target) {
$target = (array)$target;
foreach ($target as $phid) {
$phids[] = $phid;
}
}
}
foreach ($rule->getConditions() as $condition) {
$value = $condition->getValue();
if (is_array($value)) {
foreach ($value as $phid) {
$phids[] = $phid;
}
}
}
$phids[] = $rule->getAuthorPHID();
if ($rule->isObjectRule()) {
$phids[] = $rule->getTriggerObjectPHID();
}
return $this->loadViewerHandles($phids);
}
/**
* Render the selector for the "When (all of | any of) these conditions are
* met:" element.
*/
private function renderMustMatchSelector($rule) {
return AphrontFormSelectControl::renderSelectTag(
$rule->getMustMatchAll() ? 'all' : 'any',
array(
'all' => pht('all of'),
'any' => pht('any of'),
),
array(
'name' => 'must_match',
));
}
/**
* Render the selector for "Take these actions (every time | only the first
* time) this rule matches..." element.
*/
private function renderRepetitionSelector($rule, HeraldAdapter $adapter) {
$repetition_policy = $rule->getRepetitionPolicyStringConstant();
$repetition_map = $this->getRepetitionOptionMap($adapter);
if (count($repetition_map) < 2) {
return head($repetition_map);
} else {
return AphrontFormSelectControl::renderSelectTag(
$repetition_policy,
$repetition_map,
array(
'name' => 'repetition_policy',
));
}
}
private function getRepetitionOptionMap(HeraldAdapter $adapter) {
$repetition_options = $adapter->getRepetitionOptions();
$repetition_names = HeraldRule::getRepetitionPolicySelectOptionMap();
return array_select_keys($repetition_names, $repetition_options);
}
protected function buildTokenizerTemplates() {
$template = new AphrontTokenizerTemplateView();
$template = $template->render();
return array(
'markup' => $template,
);
}
/**
* Load rules for the "Another Herald rule..." condition dropdown, which
* allows one rule to depend upon the success or failure of another rule.
*/
private function loadRulesThisRuleMayDependUpon(HeraldRule $rule) {
$viewer = $this->getRequest()->getUser();
// Any rule can depend on a global rule.
$all_rules = id(new HeraldRuleQuery())
->setViewer($viewer)
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_GLOBAL))
->withContentTypes(array($rule->getContentType()))
->execute();
if ($rule->isObjectRule()) {
// Object rules may depend on other rules for the same object.
$all_rules += id(new HeraldRuleQuery())
->setViewer($viewer)
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_OBJECT))
->withContentTypes(array($rule->getContentType()))
->withTriggerObjectPHIDs(array($rule->getTriggerObjectPHID()))
->execute();
}
if ($rule->isPersonalRule()) {
// Personal rules may depend upon your other personal rules.
$all_rules += id(new HeraldRuleQuery())
->setViewer($viewer)
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_PERSONAL))
->withContentTypes(array($rule->getContentType()))
->withAuthorPHIDs(array($rule->getAuthorPHID()))
->execute();
}
// mark disabled rules as disabled since they are not useful as such;
// don't filter though to keep edit cases sane / expected
foreach ($all_rules as $current_rule) {
if ($current_rule->getIsDisabled()) {
$current_rule->makeEphemeral();
$current_rule->setName($rule->getName().' '.pht('(Disabled)'));
}
}
// A rule can not depend upon itself.
unset($all_rules[$rule->getID()]);
return $all_rules;
}
private function getFieldGroups(HeraldAdapter $adapter, array $field_map) {
$group_map = array();
foreach ($field_map as $field_key => $field_name) {
$group_key = $adapter->getFieldGroupKey($field_key);
$group_map[$group_key][$field_key] = $field_name;
}
return $this->getGroups(
$group_map,
HeraldFieldGroup::getAllFieldGroups());
}
private function getActionGroups(HeraldAdapter $adapter, array $action_map) {
$group_map = array();
foreach ($action_map as $action_key => $action_name) {
$group_key = $adapter->getActionGroupKey($action_key);
$group_map[$group_key][$action_key] = $action_name;
}
return $this->getGroups(
$group_map,
HeraldActionGroup::getAllActionGroups());
}
private function getGroups(array $item_map, array $group_list) {
assert_instances_of($group_list, 'HeraldGroup');
$groups = array();
foreach ($item_map as $group_key => $options) {
asort($options);
$group_object = idx($group_list, $group_key);
if ($group_object) {
$group_label = $group_object->getGroupLabel();
$group_order = $group_object->getSortKey();
} else {
$group_label = nonempty($group_key, pht('Other'));
$group_order = 'Z';
}
$groups[] = array(
'label' => $group_label,
'options' => $options,
'order' => $group_order,
);
}
return array_values(isort($groups, 'order'));
}
}
diff --git a/src/applications/herald/controller/HeraldWebhookViewController.php b/src/applications/herald/controller/HeraldWebhookViewController.php
index d8e5eb3c5..5f6be9816 100644
--- a/src/applications/herald/controller/HeraldWebhookViewController.php
+++ b/src/applications/herald/controller/HeraldWebhookViewController.php
@@ -1,197 +1,238 @@
<?php
final class HeraldWebhookViewController
extends HeraldWebhookController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$hook = id(new HeraldWebhookQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->executeOne();
if (!$hook) {
return new Aphront404Response();
}
$header = $this->buildHeaderView($hook);
$warnings = null;
if ($hook->isInErrorBackoff($viewer)) {
$message = pht(
'Many requests to this webhook have failed recently (at least %s '.
'errors in the last %s seconds). New requests are temporarily paused.',
$hook->getErrorBackoffThreshold(),
$hook->getErrorBackoffWindow());
$warnings = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(
array(
$message,
));
}
$curtain = $this->buildCurtain($hook);
$properties_view = $this->buildPropertiesView($hook);
$timeline = $this->buildTransactionTimeline(
$hook,
new HeraldWebhookTransactionQuery());
$timeline->setShouldTerminate(true);
$requests = id(new HeraldWebhookRequestQuery())
->setViewer($viewer)
->withWebhookPHIDs(array($hook->getPHID()))
->setLimit(20)
->execute();
$warnings = array();
if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
$message = pht(
'Phabricator is currently configured in silent mode, so it will not '.
'publish webhooks. To adjust this setting, see '.
'@{config:phabricator.silent} in Config.');
$warnings[] = id(new PHUIInfoView())
->setTitle(pht('Silent Mode'))
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->appendChild(new PHUIRemarkupView($viewer, $message));
}
$requests_table = id(new HeraldWebhookRequestListView())
->setViewer($viewer)
->setRequests($requests)
->setHighlightID($request->getURIData('requestID'));
$requests_view = id(new PHUIObjectBoxView())
->setHeaderText(pht('Recent Requests'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($requests_table);
+ $rules_view = $this->newRulesView($hook);
+
$hook_view = id(new PHUITwoColumnView())
->setHeader($header)
->setMainColumn(
array(
$warnings,
$properties_view,
+ $rules_view,
$requests_view,
$timeline,
))
->setCurtain($curtain);
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb(pht('Webhook %d', $hook->getID()))
->setBorder(true);
return $this->newPage()
->setTitle(
array(
pht('Webhook %d', $hook->getID()),
$hook->getName(),
))
->setCrumbs($crumbs)
->setPageObjectPHIDs(
array(
$hook->getPHID(),
))
->appendChild($hook_view);
}
private function buildHeaderView(HeraldWebhook $hook) {
$viewer = $this->getViewer();
$title = $hook->getName();
$status_icon = $hook->getStatusIcon();
$status_color = $hook->getStatusColor();
$status_name = $hook->getStatusDisplayName();
$header = id(new PHUIHeaderView())
->setHeader($title)
->setViewer($viewer)
->setPolicyObject($hook)
->setStatus($status_icon, $status_color, $status_name)
->setHeaderIcon('fa-cloud-upload');
return $header;
}
private function buildCurtain(HeraldWebhook $hook) {
$viewer = $this->getViewer();
$curtain = $this->newCurtainView($hook);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$hook,
PhabricatorPolicyCapability::CAN_EDIT);
$id = $hook->getID();
$edit_uri = $this->getApplicationURI("webhook/edit/{$id}/");
$test_uri = $this->getApplicationURI("webhook/test/{$id}/");
$key_view_uri = $this->getApplicationURI("webhook/key/view/{$id}/");
$key_cycle_uri = $this->getApplicationURI("webhook/key/cycle/{$id}/");
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Webhook'))
->setIcon('fa-pencil')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setHref($edit_uri));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('New Test Request'))
->setIcon('fa-cloud-upload')
->setDisabled(!$can_edit)
->setWorkflow(true)
->setHref($test_uri));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('View HMAC Key'))
->setIcon('fa-key')
->setDisabled(!$can_edit)
->setWorkflow(true)
->setHref($key_view_uri));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Regenerate HMAC Key'))
->setIcon('fa-refresh')
->setDisabled(!$can_edit)
->setWorkflow(true)
->setHref($key_cycle_uri));
return $curtain;
}
private function buildPropertiesView(HeraldWebhook $hook) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setViewer($viewer);
$properties->addProperty(
pht('URI'),
$hook->getWebhookURI());
$properties->addProperty(
pht('Status'),
$hook->getStatusDisplayName());
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Details'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($properties);
}
+ private function newRulesView(HeraldWebhook $hook) {
+ $viewer = $this->getViewer();
+
+ $rules = id(new HeraldRuleQuery())
+ ->setViewer($viewer)
+ ->withDisabled(false)
+ ->withAffectedObjectPHIDs(array($hook->getPHID()))
+ ->needValidateAuthors(true)
+ ->setLimit(10)
+ ->execute();
+
+ $list = id(new HeraldRuleListView())
+ ->setViewer($viewer)
+ ->setRules($rules)
+ ->newObjectList();
+
+ $list->setNoDataString(pht('No active Herald rules call this webhook.'));
+
+ $more_href = new PhutilURI(
+ '/herald/',
+ array('affectedPHID' => $hook->getPHID()));
+
+ $more_link = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setIcon('fa-list-ul')
+ ->setText(pht('View All Rules'))
+ ->setHref($more_href);
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader(pht('Called By Herald Rules'))
+ ->addActionLink($more_link);
+
+ return id(new PHUIObjectBoxView())
+ ->setHeader($header)
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->appendChild($list);
+ }
+
}
diff --git a/src/applications/herald/edge/HeraldRuleActionAffectsObjectEdgeType.php b/src/applications/herald/edge/HeraldRuleActionAffectsObjectEdgeType.php
new file mode 100644
index 000000000..35a30773a
--- /dev/null
+++ b/src/applications/herald/edge/HeraldRuleActionAffectsObjectEdgeType.php
@@ -0,0 +1,8 @@
+<?php
+
+final class HeraldRuleActionAffectsObjectEdgeType
+ extends PhabricatorEdgeType {
+
+ const EDGECONST = 69;
+
+}
diff --git a/src/applications/herald/editor/HeraldRuleEditor.php b/src/applications/herald/editor/HeraldRuleEditor.php
index 3ba5c4f8a..1039d7432 100644
--- a/src/applications/herald/editor/HeraldRuleEditor.php
+++ b/src/applications/herald/editor/HeraldRuleEditor.php
@@ -1,154 +1,81 @@
<?php
final class HeraldRuleEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorHeraldApplication';
}
public function getEditorObjectsDescription() {
return pht('Herald Rules');
}
- public function getTransactionTypes() {
- $types = parent::getTransactionTypes();
-
- $types[] = PhabricatorTransactions::TYPE_COMMENT;
- $types[] = HeraldRuleTransaction::TYPE_EDIT;
- $types[] = HeraldRuleTransaction::TYPE_NAME;
- $types[] = HeraldRuleTransaction::TYPE_DISABLE;
-
- return $types;
- }
-
- protected function getCustomTransactionOldValue(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
-
- switch ($xaction->getTransactionType()) {
- case HeraldRuleTransaction::TYPE_DISABLE:
- return (int)$object->getIsDisabled();
- case HeraldRuleTransaction::TYPE_EDIT:
- return id(new HeraldRuleSerializer())
- ->serializeRule($object);
- case HeraldRuleTransaction::TYPE_NAME:
- return $object->getName();
- }
-
- }
-
- protected function getCustomTransactionNewValue(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
-
- switch ($xaction->getTransactionType()) {
- case HeraldRuleTransaction::TYPE_DISABLE:
- return (int)$xaction->getNewValue();
- case HeraldRuleTransaction::TYPE_EDIT:
- case HeraldRuleTransaction::TYPE_NAME:
- return $xaction->getNewValue();
- }
- }
-
- protected function applyCustomInternalTransaction(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
-
- switch ($xaction->getTransactionType()) {
- case HeraldRuleTransaction::TYPE_DISABLE:
- return $object->setIsDisabled($xaction->getNewValue());
- case HeraldRuleTransaction::TYPE_NAME:
- return $object->setName($xaction->getNewValue());
- case HeraldRuleTransaction::TYPE_EDIT:
- $new_state = id(new HeraldRuleSerializer())
- ->deserializeRuleComponents($xaction->getNewValue());
- $object->setMustMatchAll((int)$new_state['match_all']);
- $object->attachConditions($new_state['conditions']);
- $object->attachActions($new_state['actions']);
-
- $new_repetition = $new_state['repetition_policy'];
- $object->setRepetitionPolicyStringConstant($new_repetition);
-
- return $object;
- }
-
- }
-
- protected function applyCustomExternalTransaction(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
- switch ($xaction->getTransactionType()) {
- case HeraldRuleTransaction::TYPE_EDIT:
- $object->saveConditions($object->getConditions());
- $object->saveActions($object->getActions());
- break;
- }
- return;
- }
-
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new HeraldRuleAdapter())
->setRule($object);
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$phids = array();
$phids[] = $this->getActingAsPHID();
if ($object->isPersonalRule()) {
$phids[] = $object->getAuthorPHID();
}
return $phids;
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new HeraldRuleReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$monogram = $object->getMonogram();
$name = $object->getName();
$subject = pht('%s: %s', $monogram, $name);
return id(new PhabricatorMetaMTAMail())
->setSubject($subject);
}
protected function getMailSubjectPrefix() {
return pht('[Herald]');
}
-
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$body->addLinkSection(
pht('RULE DETAIL'),
PhabricatorEnv::getProductionURI($object->getURI()));
return $body;
}
+ protected function supportsSearch() {
+ return true;
+ }
+
}
diff --git a/src/applications/herald/engineextension/HeraldRuleIndexEngineExtension.php b/src/applications/herald/engineextension/HeraldRuleIndexEngineExtension.php
new file mode 100644
index 000000000..7b7b2fb52
--- /dev/null
+++ b/src/applications/herald/engineextension/HeraldRuleIndexEngineExtension.php
@@ -0,0 +1,92 @@
+<?php
+
+final class HeraldRuleIndexEngineExtension
+ extends PhabricatorIndexEngineExtension {
+
+ const EXTENSIONKEY = 'herald.actions';
+
+ public function getExtensionName() {
+ return pht('Herald Actions');
+ }
+
+ public function shouldIndexObject($object) {
+ if (!($object instanceof HeraldRule)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function indexObject(
+ PhabricatorIndexEngine $engine,
+ $object) {
+
+ $edge_type = HeraldRuleActionAffectsObjectEdgeType::EDGECONST;
+
+ $old_edges = PhabricatorEdgeQuery::loadDestinationPHIDs(
+ $object->getPHID(),
+ $edge_type);
+ $old_edges = array_fuse($old_edges);
+
+ $new_edges = $this->getPHIDsAffectedByActions($object);
+ $new_edges = array_fuse($new_edges);
+
+ $add_edges = array_diff_key($new_edges, $old_edges);
+ $rem_edges = array_diff_key($old_edges, $new_edges);
+
+ if (!$add_edges && !$rem_edges) {
+ return;
+ }
+
+ $editor = new PhabricatorEdgeEditor();
+
+ foreach ($add_edges as $phid) {
+ $editor->addEdge($object->getPHID(), $edge_type, $phid);
+ }
+
+ foreach ($rem_edges as $phid) {
+ $editor->removeEdge($object->getPHID(), $edge_type, $phid);
+ }
+
+ $editor->save();
+ }
+
+ public function getIndexVersion($object) {
+ $phids = $this->getPHIDsAffectedByActions($object);
+ sort($phids);
+ $phids = implode(':', $phids);
+ return PhabricatorHash::digestForIndex($phids);
+ }
+
+ private function getPHIDsAffectedByActions(HeraldRule $rule) {
+ $viewer = $this->getViewer();
+
+ $rule = id(new HeraldRuleQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($rule->getID()))
+ ->needConditionsAndActions(true)
+ ->executeOne();
+ if (!$rule) {
+ return array();
+ }
+
+ $phids = array();
+
+ $actions = HeraldAction::getAllActions();
+ foreach ($rule->getActions() as $action_record) {
+ $action = idx($actions, $action_record->getAction());
+
+ if (!$action) {
+ continue;
+ }
+
+ foreach ($action->getPHIDsAffectedByAction($action_record) as $phid) {
+ $phids[] = $phid;
+ }
+ }
+
+ $phids = array_fuse($phids);
+ return array_keys($phids);
+ }
+
+}
diff --git a/src/applications/herald/query/HeraldRuleQuery.php b/src/applications/herald/query/HeraldRuleQuery.php
index e6dba43c7..e346a998d 100644
--- a/src/applications/herald/query/HeraldRuleQuery.php
+++ b/src/applications/herald/query/HeraldRuleQuery.php
@@ -1,313 +1,341 @@
<?php
final class HeraldRuleQuery extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $authorPHIDs;
private $ruleTypes;
private $contentTypes;
private $disabled;
private $active;
private $datasourceQuery;
private $triggerObjectPHIDs;
+ private $affectedObjectPHIDs;
private $needConditionsAndActions;
private $needAppliedToPHIDs;
private $needValidateAuthors;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAuthorPHIDs(array $author_phids) {
$this->authorPHIDs = $author_phids;
return $this;
}
public function withRuleTypes(array $types) {
$this->ruleTypes = $types;
return $this;
}
public function withContentTypes(array $types) {
$this->contentTypes = $types;
return $this;
}
public function withDisabled($disabled) {
$this->disabled = $disabled;
return $this;
}
public function withActive($active) {
$this->active = $active;
return $this;
}
public function withDatasourceQuery($query) {
$this->datasourceQuery = $query;
return $this;
}
public function withTriggerObjectPHIDs(array $phids) {
$this->triggerObjectPHIDs = $phids;
return $this;
}
+ public function withAffectedObjectPHIDs(array $phids) {
+ $this->affectedObjectPHIDs = $phids;
+ return $this;
+ }
+
public function needConditionsAndActions($need) {
$this->needConditionsAndActions = $need;
return $this;
}
public function needAppliedToPHIDs(array $phids) {
$this->needAppliedToPHIDs = $phids;
return $this;
}
public function needValidateAuthors($need) {
$this->needValidateAuthors = $need;
return $this;
}
public function newResultObject() {
return new HeraldRule();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $rules) {
$rule_ids = mpull($rules, 'getID');
// Filter out any rules that have invalid adapters, or have adapters the
// viewer isn't permitted to see or use (for example, Differential rules
// if the user can't use Differential or Differential is not installed).
$types = HeraldAdapter::getEnabledAdapterMap($this->getViewer());
foreach ($rules as $key => $rule) {
if (empty($types[$rule->getContentType()])) {
$this->didRejectResult($rule);
unset($rules[$key]);
}
}
if ($this->needValidateAuthors || ($this->active !== null)) {
$this->validateRuleAuthors($rules);
}
if ($this->active !== null) {
$need_active = (bool)$this->active;
foreach ($rules as $key => $rule) {
if ($rule->getIsDisabled()) {
$is_active = false;
} else if (!$rule->hasValidAuthor()) {
$is_active = false;
} else {
$is_active = true;
}
if ($is_active != $need_active) {
unset($rules[$key]);
}
}
}
if (!$rules) {
return array();
}
if ($this->needConditionsAndActions) {
$conditions = id(new HeraldCondition())->loadAllWhere(
'ruleID IN (%Ld)',
$rule_ids);
$conditions = mgroup($conditions, 'getRuleID');
$actions = id(new HeraldActionRecord())->loadAllWhere(
'ruleID IN (%Ld)',
$rule_ids);
$actions = mgroup($actions, 'getRuleID');
foreach ($rules as $rule) {
$rule->attachActions(idx($actions, $rule->getID(), array()));
$rule->attachConditions(idx($conditions, $rule->getID(), array()));
}
}
if ($this->needAppliedToPHIDs) {
$conn_r = id(new HeraldRule())->establishConnection('r');
$applied = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE ruleID IN (%Ld) AND phid IN (%Ls)',
HeraldRule::TABLE_RULE_APPLIED,
$rule_ids,
$this->needAppliedToPHIDs);
$map = array();
foreach ($applied as $row) {
$map[$row['ruleID']][$row['phid']] = true;
}
foreach ($rules as $rule) {
foreach ($this->needAppliedToPHIDs as $phid) {
$rule->setRuleApplied(
$phid,
isset($map[$rule->getID()][$phid]));
}
}
}
$object_phids = array();
foreach ($rules as $rule) {
if ($rule->isObjectRule()) {
$object_phids[] = $rule->getTriggerObjectPHID();
}
}
if ($object_phids) {
$objects = id(new PhabricatorObjectQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($object_phids)
->execute();
$objects = mpull($objects, null, 'getPHID');
} else {
$objects = array();
}
foreach ($rules as $key => $rule) {
if ($rule->isObjectRule()) {
$object = idx($objects, $rule->getTriggerObjectPHID());
if (!$object) {
unset($rules[$key]);
continue;
}
$rule->attachTriggerObject($object);
}
}
return $rules;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'rule.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'rule.phid IN (%Ls)',
$this->phids);
}
if ($this->authorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'rule.authorPHID IN (%Ls)',
$this->authorPHIDs);
}
if ($this->ruleTypes !== null) {
$where[] = qsprintf(
$conn,
'rule.ruleType IN (%Ls)',
$this->ruleTypes);
}
if ($this->contentTypes !== null) {
$where[] = qsprintf(
$conn,
'rule.contentType IN (%Ls)',
$this->contentTypes);
}
if ($this->disabled !== null) {
$where[] = qsprintf(
$conn,
'rule.isDisabled = %d',
(int)$this->disabled);
}
if ($this->active !== null) {
$where[] = qsprintf(
$conn,
'rule.isDisabled = %d',
(int)(!$this->active));
}
if ($this->datasourceQuery !== null) {
$where[] = qsprintf(
$conn,
'rule.name LIKE %>',
$this->datasourceQuery);
}
if ($this->triggerObjectPHIDs !== null) {
$where[] = qsprintf(
$conn,
'rule.triggerObjectPHID IN (%Ls)',
$this->triggerObjectPHIDs);
}
+ if ($this->affectedObjectPHIDs !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'edge_affects.dst IN (%Ls)',
+ $this->affectedObjectPHIDs);
+ }
+
return $where;
}
+ protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
+ $joins = parent::buildJoinClauseParts($conn);
+
+ if ($this->affectedObjectPHIDs !== null) {
+ $joins[] = qsprintf(
+ $conn,
+ 'JOIN %T edge_affects ON rule.phid = edge_affects.src
+ AND edge_affects.type = %d',
+ PhabricatorEdgeConfig::TABLE_NAME_EDGE,
+ HeraldRuleActionAffectsObjectEdgeType::EDGECONST);
+ }
+
+ return $joins;
+ }
+
private function validateRuleAuthors(array $rules) {
// "Global" and "Object" rules always have valid authors.
foreach ($rules as $key => $rule) {
if ($rule->isGlobalRule() || $rule->isObjectRule()) {
$rule->attachValidAuthor(true);
unset($rules[$key]);
continue;
}
}
if (!$rules) {
return;
}
// For personal rules, the author needs to exist and not be disabled.
$user_phids = mpull($rules, 'getAuthorPHID');
$users = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withPHIDs($user_phids)
->execute();
$users = mpull($users, null, 'getPHID');
foreach ($rules as $key => $rule) {
$author_phid = $rule->getAuthorPHID();
if (empty($users[$author_phid])) {
$rule->attachValidAuthor(false);
continue;
}
if (!$users[$author_phid]->isUserActivated()) {
$rule->attachValidAuthor(false);
continue;
}
$rule->attachValidAuthor(true);
$rule->attachAuthor($users[$author_phid]);
}
}
public function getQueryApplicationClass() {
return 'PhabricatorHeraldApplication';
}
protected function getPrimaryTableAlias() {
return 'rule';
}
}
diff --git a/src/applications/herald/query/HeraldRuleSearchEngine.php b/src/applications/herald/query/HeraldRuleSearchEngine.php
index 47a683273..95e307971 100644
--- a/src/applications/herald/query/HeraldRuleSearchEngine.php
+++ b/src/applications/herald/query/HeraldRuleSearchEngine.php
@@ -1,200 +1,172 @@
<?php
final class HeraldRuleSearchEngine extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Herald Rules');
}
public function getApplicationClassName() {
return 'PhabricatorHeraldApplication';
}
public function newQuery() {
return id(new HeraldRuleQuery())
->needValidateAuthors(true);
}
protected function buildCustomSearchFields() {
$viewer = $this->requireViewer();
$rule_types = HeraldRuleTypeConfig::getRuleTypeMap();
$content_types = HeraldAdapter::getEnabledAdapterMap($viewer);
return array(
id(new PhabricatorUsersSearchField())
->setLabel(pht('Authors'))
->setKey('authorPHIDs')
->setAliases(array('author', 'authors', 'authorPHID'))
->setDescription(
pht('Search for rules with given authors.')),
id(new PhabricatorSearchCheckboxesField())
->setKey('ruleTypes')
->setAliases(array('ruleType'))
->setLabel(pht('Rule Type'))
->setDescription(
pht('Search for rules of given types.'))
->setOptions($rule_types),
id(new PhabricatorSearchCheckboxesField())
->setKey('contentTypes')
->setLabel(pht('Content Type'))
->setDescription(
pht('Search for rules affecting given types of content.'))
->setOptions($content_types),
id(new PhabricatorSearchThreeStateField())
->setLabel(pht('Active Rules'))
->setKey('active')
->setOptions(
pht('(Show All)'),
pht('Show Only Active Rules'),
pht('Show Only Inactive Rules')),
id(new PhabricatorSearchThreeStateField())
->setLabel(pht('Disabled Rules'))
->setKey('disabled')
->setOptions(
pht('(Show All)'),
pht('Show Only Disabled Rules'),
pht('Show Only Enabled Rules')),
+ id(new PhabricatorPHIDsSearchField())
+ ->setLabel(pht('Affected Objects'))
+ ->setKey('affectedPHIDs')
+ ->setAliases(array('affectedPHID')),
);
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
if ($map['authorPHIDs']) {
$query->withAuthorPHIDs($map['authorPHIDs']);
}
if ($map['contentTypes']) {
$query->withContentTypes($map['contentTypes']);
}
if ($map['ruleTypes']) {
$query->withRuleTypes($map['ruleTypes']);
}
if ($map['disabled'] !== null) {
$query->withDisabled($map['disabled']);
}
if ($map['active'] !== null) {
$query->withActive($map['active']);
}
+ if ($map['affectedPHIDs']) {
+ $query->withAffectedObjectPHIDs($map['affectedPHIDs']);
+ }
+
return $query;
}
protected function getURI($path) {
return '/herald/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array();
if ($this->requireViewer()->isLoggedIn()) {
$names['authored'] = pht('Authored');
}
$names['active'] = pht('Active');
$names['all'] = pht('All');
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
$viewer_phid = $this->requireViewer()->getPHID();
switch ($query_key) {
case 'all':
return $query;
case 'active':
return $query
->setParameter('active', true);
case 'authored':
return $query
->setParameter('authorPHIDs', array($viewer_phid))
->setParameter('disabled', false);
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function renderResultList(
array $rules,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($rules, 'HeraldRule');
-
$viewer = $this->requireViewer();
- $handles = $viewer->loadHandles(mpull($rules, 'getAuthorPHID'));
-
- $content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer);
-
- $list = id(new PHUIObjectItemListView())
- ->setUser($viewer);
- foreach ($rules as $rule) {
- $monogram = $rule->getMonogram();
-
- $item = id(new PHUIObjectItemView())
- ->setObjectName($monogram)
- ->setHeader($rule->getName())
- ->setHref("/{$monogram}");
-
- if ($rule->isPersonalRule()) {
- $item->addIcon('fa-user', pht('Personal Rule'));
- $item->addByline(
- pht(
- 'Authored by %s',
- $handles[$rule->getAuthorPHID()]->renderLink()));
- } else if ($rule->isObjectRule()) {
- $item->addIcon('fa-briefcase', pht('Object Rule'));
- } else {
- $item->addIcon('fa-globe', pht('Global Rule'));
- }
-
- if ($rule->getIsDisabled()) {
- $item->setDisabled(true);
- $item->addIcon('fa-lock grey', pht('Disabled'));
- } else if (!$rule->hasValidAuthor()) {
- $item->setDisabled(true);
- $item->addIcon('fa-user grey', pht('Author Not Active'));
- }
-
- $content_type_name = idx($content_type_map, $rule->getContentType());
- $item->addAttribute(pht('Affects: %s', $content_type_name));
-
- $list->addItem($item);
- }
+
+ $list = id(new HeraldRuleListView())
+ ->setViewer($viewer)
+ ->setRules($rules)
+ ->newObjectList();
$result = new PhabricatorApplicationSearchResultView();
$result->setObjectList($list);
$result->setNoDataString(pht('No rules found.'));
return $result;
-
}
protected function getNewUserBody() {
$create_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Create Herald Rule'))
->setHref('/herald/create/')
->setColor(PHUIButtonView::GREEN);
$icon = $this->getApplication()->getIcon();
$app_name = $this->getApplication()->getName();
$view = id(new PHUIBigInfoView())
->setIcon($icon)
->setTitle(pht('Welcome to %s', $app_name))
->setDescription(
pht('A flexible rules engine that can notify and act on '.
'other actions such as tasks, diffs, and commits.'))
->addAction($create_button);
return $view;
}
}
diff --git a/src/applications/herald/storage/HeraldRule.php b/src/applications/herald/storage/HeraldRule.php
index 1b005898c..a9c131e71 100644
--- a/src/applications/herald/storage/HeraldRule.php
+++ b/src/applications/herald/storage/HeraldRule.php
@@ -1,393 +1,394 @@
<?php
final class HeraldRule extends HeraldDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
+ PhabricatorIndexableInterface,
PhabricatorSubscribableInterface {
const TABLE_RULE_APPLIED = 'herald_ruleapplied';
protected $name;
protected $authorPHID;
protected $contentType;
protected $mustMatchAll;
protected $repetitionPolicy;
protected $ruleType;
protected $isDisabled = 0;
protected $triggerObjectPHID;
protected $configVersion = 38;
// PHIDs for which this rule has been applied
private $ruleApplied = self::ATTACHABLE;
private $validAuthor = self::ATTACHABLE;
private $author = self::ATTACHABLE;
private $conditions;
private $actions;
private $triggerObject = self::ATTACHABLE;
const REPEAT_EVERY = 'every';
const REPEAT_FIRST = 'first';
const REPEAT_CHANGE = 'change';
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort255',
'contentType' => 'text255',
'mustMatchAll' => 'bool',
'configVersion' => 'uint32',
'repetitionPolicy' => 'text32',
'ruleType' => 'text32',
'isDisabled' => 'uint32',
'triggerObjectPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_name' => array(
'columns' => array('name(128)'),
),
'key_author' => array(
'columns' => array('authorPHID'),
),
'key_ruletype' => array(
'columns' => array('ruleType'),
),
'key_trigger' => array(
'columns' => array('triggerObjectPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(HeraldRulePHIDType::TYPECONST);
}
public function getRuleApplied($phid) {
return $this->assertAttachedKey($this->ruleApplied, $phid);
}
public function setRuleApplied($phid, $applied) {
if ($this->ruleApplied === self::ATTACHABLE) {
$this->ruleApplied = array();
}
$this->ruleApplied[$phid] = $applied;
return $this;
}
public function loadConditions() {
if (!$this->getID()) {
return array();
}
return id(new HeraldCondition())->loadAllWhere(
'ruleID = %d',
$this->getID());
}
public function attachConditions(array $conditions) {
assert_instances_of($conditions, 'HeraldCondition');
$this->conditions = $conditions;
return $this;
}
public function getConditions() {
// TODO: validate conditions have been attached.
return $this->conditions;
}
public function loadActions() {
if (!$this->getID()) {
return array();
}
return id(new HeraldActionRecord())->loadAllWhere(
'ruleID = %d',
$this->getID());
}
public function attachActions(array $actions) {
// TODO: validate actions have been attached.
assert_instances_of($actions, 'HeraldActionRecord');
$this->actions = $actions;
return $this;
}
public function getActions() {
return $this->actions;
}
public function saveConditions(array $conditions) {
assert_instances_of($conditions, 'HeraldCondition');
return $this->saveChildren(
id(new HeraldCondition())->getTableName(),
$conditions);
}
public function saveActions(array $actions) {
assert_instances_of($actions, 'HeraldActionRecord');
return $this->saveChildren(
id(new HeraldActionRecord())->getTableName(),
$actions);
}
protected function saveChildren($table_name, array $children) {
assert_instances_of($children, 'HeraldDAO');
if (!$this->getID()) {
throw new PhutilInvalidStateException('save');
}
foreach ($children as $child) {
$child->setRuleID($this->getID());
}
$this->openTransaction();
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE ruleID = %d',
$table_name,
$this->getID());
foreach ($children as $child) {
$child->save();
}
$this->saveTransaction();
}
public function delete() {
$this->openTransaction();
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE ruleID = %d',
id(new HeraldCondition())->getTableName(),
$this->getID());
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE ruleID = %d',
id(new HeraldActionRecord())->getTableName(),
$this->getID());
$result = parent::delete();
$this->saveTransaction();
return $result;
}
public function hasValidAuthor() {
return $this->assertAttached($this->validAuthor);
}
public function attachValidAuthor($valid) {
$this->validAuthor = $valid;
return $this;
}
public function getAuthor() {
return $this->assertAttached($this->author);
}
public function attachAuthor(PhabricatorUser $user) {
$this->author = $user;
return $this;
}
public function isGlobalRule() {
return ($this->getRuleType() === HeraldRuleTypeConfig::RULE_TYPE_GLOBAL);
}
public function isPersonalRule() {
return ($this->getRuleType() === HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
}
public function isObjectRule() {
return ($this->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_OBJECT);
}
public function attachTriggerObject($trigger_object) {
$this->triggerObject = $trigger_object;
return $this;
}
public function getTriggerObject() {
return $this->assertAttached($this->triggerObject);
}
/**
* Get a sortable key for rule execution order.
*
* Rules execute in a well-defined order: personal rules first, then object
* rules, then global rules. Within each rule type, rules execute from lowest
* ID to highest ID.
*
* This ordering allows more powerful rules (like global rules) to override
* weaker rules (like personal rules) when multiple rules exist which try to
* affect the same field. Executing from low IDs to high IDs makes
* interactions easier to understand when adding new rules, because the newest
* rules always happen last.
*
* @return string A sortable key for this rule.
*/
public function getRuleExecutionOrderSortKey() {
$rule_type = $this->getRuleType();
switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
$type_order = 1;
break;
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
$type_order = 2;
break;
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
$type_order = 3;
break;
default:
throw new Exception(pht('Unknown rule type "%s"!', $rule_type));
}
return sprintf('~%d%010d', $type_order, $this->getID());
}
public function getMonogram() {
return 'H'.$this->getID();
}
public function getURI() {
return '/'.$this->getMonogram();
}
/* -( Repetition Policies )------------------------------------------------ */
public function getRepetitionPolicyStringConstant() {
return $this->getRepetitionPolicy();
}
public function setRepetitionPolicyStringConstant($value) {
$map = self::getRepetitionPolicyMap();
if (!isset($map[$value])) {
throw new Exception(
pht(
'Rule repetition string constant "%s" is unknown.',
$value));
}
return $this->setRepetitionPolicy($value);
}
public function isRepeatEvery() {
return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_EVERY);
}
public function isRepeatFirst() {
return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_FIRST);
}
public function isRepeatOnChange() {
return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_CHANGE);
}
public static function getRepetitionPolicySelectOptionMap() {
$map = self::getRepetitionPolicyMap();
return ipull($map, 'select');
}
private static function getRepetitionPolicyMap() {
return array(
self::REPEAT_EVERY => array(
'select' => pht('every time this rule matches:'),
),
self::REPEAT_FIRST => array(
'select' => pht('only the first time this rule matches:'),
),
self::REPEAT_CHANGE => array(
'select' => pht('if this rule did not match the last time:'),
),
);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new HeraldRuleEditor();
}
public function getApplicationTransactionTemplate() {
return new HeraldRuleTransaction();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
return PhabricatorPolicies::getMostOpenPolicy();
}
if ($this->isGlobalRule()) {
$app = 'PhabricatorHeraldApplication';
$herald = PhabricatorApplication::getByClass($app);
$global = HeraldManageGlobalRulesCapability::CAPABILITY;
return $herald->getPolicy($global);
} else if ($this->isObjectRule()) {
return $this->getTriggerObject()->getPolicy($capability);
} else {
return $this->getAuthorPHID();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
return null;
}
if ($this->isGlobalRule()) {
return pht(
'Global Herald rules can be edited by users with the "Can Manage '.
'Global Rules" Herald application permission.');
} else if ($this->isObjectRule()) {
return pht('Object rules inherit the edit policies of their objects.');
} else {
return pht('A personal rule can only be edited by its owner.');
}
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return $this->isPersonalRule() && $phid == $this->getAuthorPHID();
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/herald/storage/HeraldRuleTransaction.php b/src/applications/herald/storage/HeraldRuleTransaction.php
index b1bd56374..7fa7667ec 100644
--- a/src/applications/herald/storage/HeraldRuleTransaction.php
+++ b/src/applications/herald/storage/HeraldRuleTransaction.php
@@ -1,135 +1,20 @@
<?php
final class HeraldRuleTransaction
- extends PhabricatorApplicationTransaction {
+ extends PhabricatorModularTransaction {
const TYPE_EDIT = 'herald:edit';
- const TYPE_NAME = 'herald:name';
- const TYPE_DISABLE = 'herald:disable';
public function getApplicationName() {
return 'herald';
}
public function getApplicationTransactionType() {
return HeraldRulePHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return new HeraldRuleTransactionComment();
- }
-
- public function getColor() {
- $old = $this->getOldValue();
- $new = $this->getNewValue();
-
- switch ($this->getTransactionType()) {
- case self::TYPE_DISABLE:
- if ($new) {
- return 'red';
- } else {
- return 'green';
- }
- }
-
- return parent::getColor();
- }
-
- public function getActionName() {
- $old = $this->getOldValue();
- $new = $this->getNewValue();
-
- switch ($this->getTransactionType()) {
- case self::TYPE_DISABLE:
- if ($new) {
- return pht('Disabled');
- } else {
- return pht('Enabled');
- }
- case self::TYPE_NAME:
- return pht('Renamed');
- }
-
- return parent::getActionName();
- }
-
- public function getIcon() {
- $old = $this->getOldValue();
- $new = $this->getNewValue();
-
- switch ($this->getTransactionType()) {
- case self::TYPE_DISABLE:
- if ($new) {
- return 'fa-ban';
- } else {
- return 'fa-check';
- }
- }
-
- return parent::getIcon();
- }
-
-
- public function getTitle() {
- $author_phid = $this->getAuthorPHID();
-
- $old = $this->getOldValue();
- $new = $this->getNewValue();
-
- switch ($this->getTransactionType()) {
- case self::TYPE_DISABLE:
- if ($new) {
- return pht(
- '%s disabled this rule.',
- $this->renderHandleLink($author_phid));
- } else {
- return pht(
- '%s enabled this rule.',
- $this->renderHandleLink($author_phid));
- }
- case self::TYPE_NAME:
- if ($old == null) {
- return pht(
- '%s created this rule.',
- $this->renderHandleLink($author_phid));
- } else {
- return pht(
- '%s renamed this rule from "%s" to "%s".',
- $this->renderHandleLink($author_phid),
- $old,
- $new);
- }
- case self::TYPE_EDIT:
- return pht(
- '%s edited this rule.',
- $this->renderHandleLink($author_phid));
- }
-
- return parent::getTitle();
- }
-
- public function hasChangeDetails() {
- switch ($this->getTransactionType()) {
- case self::TYPE_EDIT:
- return true;
- }
- return parent::hasChangeDetails();
- }
-
- public function renderChangeDetails(PhabricatorUser $viewer) {
- $json = new PhutilJSON();
- switch ($this->getTransactionType()) {
- case self::TYPE_EDIT:
- return $this->renderTextCorpusChangeDetails(
- $viewer,
- $json->encodeFormatted($this->getOldValue()),
- $json->encodeFormatted($this->getNewValue()));
- }
-
- return $this->renderTextCorpusChangeDetails(
- $viewer,
- $this->getOldValue(),
- $this->getNewValue());
+ public function getBaseTransactionClass() {
+ return 'HeraldRuleTransactionType';
}
}
diff --git a/src/applications/herald/storage/HeraldRuleTransactionComment.php b/src/applications/herald/storage/HeraldRuleTransactionComment.php
deleted file mode 100644
index 56022ef86..000000000
--- a/src/applications/herald/storage/HeraldRuleTransactionComment.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-
-final class HeraldRuleTransactionComment
- extends PhabricatorApplicationTransactionComment {
-
- public function getApplicationTransactionObject() {
- return new HeraldRuleTransaction();
- }
-
-}
diff --git a/src/applications/herald/storage/HeraldWebhookRequest.php b/src/applications/herald/storage/HeraldWebhookRequest.php
index 3381f6a99..bc916fd60 100644
--- a/src/applications/herald/storage/HeraldWebhookRequest.php
+++ b/src/applications/herald/storage/HeraldWebhookRequest.php
@@ -1,276 +1,276 @@
<?php
final class HeraldWebhookRequest
extends HeraldDAO
implements
PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface {
protected $webhookPHID;
protected $objectPHID;
protected $status;
protected $properties = array();
protected $lastRequestResult;
protected $lastRequestEpoch;
private $webhook = self::ATTACHABLE;
const RETRY_NEVER = 'never';
const RETRY_FOREVER = 'forever';
const STATUS_QUEUED = 'queued';
const STATUS_FAILED = 'failed';
const STATUS_SENT = 'sent';
const RESULT_NONE = 'none';
const RESULT_OKAY = 'okay';
const RESULT_FAIL = 'fail';
const ERRORTYPE_HOOK = 'hook';
const ERRORTYPE_HTTP = 'http';
const ERRORTYPE_TIMEOUT = 'timeout';
const ERROR_SILENT = 'silent';
const ERROR_DISABLED = 'disabled';
const ERROR_URI = 'uri';
const ERROR_OBJECT = 'object';
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'status' => 'text32',
'lastRequestResult' => 'text32',
'lastRequestEpoch' => 'epoch',
),
self::CONFIG_KEY_SCHEMA => array(
'key_ratelimit' => array(
'columns' => array(
'webhookPHID',
'lastRequestResult',
'lastRequestEpoch',
),
),
'key_collect' => array(
'columns' => array('dateCreated'),
),
),
) + parent::getConfiguration();
}
public function getPHIDType() {
return HeraldWebhookRequestPHIDType::TYPECONST;
}
public static function initializeNewWebhookRequest(HeraldWebhook $hook) {
return id(new self())
->setWebhookPHID($hook->getPHID())
->attachWebhook($hook)
->setStatus(self::STATUS_QUEUED)
->setRetryMode(self::RETRY_NEVER)
->setLastRequestResult(self::RESULT_NONE)
->setLastRequestEpoch(0);
}
public function getWebhook() {
return $this->assertAttached($this->webhook);
}
public function attachWebhook(HeraldWebhook $hook) {
$this->webhook = $hook;
return $this;
}
protected function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
protected function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setRetryMode($mode) {
return $this->setProperty('retry', $mode);
}
public function getRetryMode() {
return $this->getProperty('retry');
}
public function setErrorType($error_type) {
return $this->setProperty('errorType', $error_type);
}
public function getErrorType() {
return $this->getProperty('errorType');
}
public function setErrorCode($error_code) {
return $this->setProperty('errorCode', $error_code);
}
public function getErrorCode() {
return $this->getProperty('errorCode');
}
public function getErrorTypeForDisplay() {
$map = array(
self::ERRORTYPE_HOOK => pht('Hook Error'),
- self::ERRORTYPE_HTTP => pht('HTTP Error'),
+ self::ERRORTYPE_HTTP => pht('HTTP Status Code'),
self::ERRORTYPE_TIMEOUT => pht('Request Timeout'),
);
$type = $this->getErrorType();
return idx($map, $type, $type);
}
public function getErrorCodeForDisplay() {
$code = $this->getErrorCode();
if ($this->getErrorType() !== self::ERRORTYPE_HOOK) {
return $code;
}
$spec = $this->getHookErrorSpec($code);
return idx($spec, 'display', $code);
}
public function setTransactionPHIDs(array $phids) {
return $this->setProperty('transactionPHIDs', $phids);
}
public function getTransactionPHIDs() {
return $this->getProperty('transactionPHIDs', array());
}
public function setTriggerPHIDs(array $phids) {
return $this->setProperty('triggerPHIDs', $phids);
}
public function getTriggerPHIDs() {
return $this->getProperty('triggerPHIDs', array());
}
public function setIsSilentAction($bool) {
return $this->setProperty('silent', $bool);
}
public function getIsSilentAction() {
return $this->getProperty('silent', false);
}
public function setIsTestAction($bool) {
return $this->setProperty('test', $bool);
}
public function getIsTestAction() {
return $this->getProperty('test', false);
}
public function setIsSecureAction($bool) {
return $this->setProperty('secure', $bool);
}
public function getIsSecureAction() {
return $this->getProperty('secure', false);
}
public function queueCall() {
PhabricatorWorker::scheduleTask(
'HeraldWebhookWorker',
array(
'webhookRequestPHID' => $this->getPHID(),
),
array(
'objectPHID' => $this->getPHID(),
));
return $this;
}
public function newStatusIcon() {
switch ($this->getStatus()) {
case self::STATUS_QUEUED:
$icon = 'fa-refresh';
$color = 'blue';
$tooltip = pht('Queued');
break;
case self::STATUS_SENT:
$icon = 'fa-check';
$color = 'green';
$tooltip = pht('Sent');
break;
case self::STATUS_FAILED:
default:
$icon = 'fa-times';
$color = 'red';
$tooltip = pht('Failed');
break;
}
return id(new PHUIIconView())
->setIcon($icon, $color)
->setTooltip($tooltip);
}
private function getHookErrorSpec($code) {
$map = $this->getHookErrorMap();
return idx($map, $code, array());
}
private function getHookErrorMap() {
return array(
self::ERROR_SILENT => array(
'display' => pht('In Silent Mode'),
),
self::ERROR_DISABLED => array(
'display' => pht('Hook Disabled'),
),
self::ERROR_URI => array(
'display' => pht('Invalid URI'),
),
self::ERROR_OBJECT => array(
'display' => pht('Invalid Object'),
),
);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
return array(
array($this->getWebhook(), PhabricatorPolicyCapability::CAN_VIEW),
);
}
}
diff --git a/src/applications/herald/storage/HeraldWebhookTransaction.php b/src/applications/herald/storage/HeraldWebhookTransaction.php
index 03c8cbb77..4f924cd4b 100644
--- a/src/applications/herald/storage/HeraldWebhookTransaction.php
+++ b/src/applications/herald/storage/HeraldWebhookTransaction.php
@@ -1,22 +1,18 @@
<?php
final class HeraldWebhookTransaction
extends PhabricatorModularTransaction {
public function getApplicationName() {
return 'herald';
}
public function getApplicationTransactionType() {
return HeraldWebhookPHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return null;
- }
-
public function getBaseTransactionClass() {
return 'HeraldWebhookTransactionType';
}
}
diff --git a/src/applications/herald/view/HeraldRuleListView.php b/src/applications/herald/view/HeraldRuleListView.php
new file mode 100644
index 000000000..150499ce8
--- /dev/null
+++ b/src/applications/herald/view/HeraldRuleListView.php
@@ -0,0 +1,65 @@
+<?php
+
+final class HeraldRuleListView
+ extends AphrontView {
+
+ private $rules;
+
+ public function setRules(array $rules) {
+ assert_instances_of($rules, 'HeraldRule');
+ $this->rules = $rules;
+ return $this;
+ }
+
+ public function render() {
+ return $this->newObjectList();
+ }
+
+ public function newObjectList() {
+ $viewer = $this->getViewer();
+ $rules = $this->rules;
+
+ $handles = $viewer->loadHandles(mpull($rules, 'getAuthorPHID'));
+
+ $content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer);
+
+ $list = id(new PHUIObjectItemListView())
+ ->setViewer($viewer);
+ foreach ($rules as $rule) {
+ $monogram = $rule->getMonogram();
+
+ $item = id(new PHUIObjectItemView())
+ ->setObjectName($monogram)
+ ->setHeader($rule->getName())
+ ->setHref($rule->getURI());
+
+ if ($rule->isPersonalRule()) {
+ $item->addIcon('fa-user', pht('Personal Rule'));
+ $item->addByline(
+ pht(
+ 'Authored by %s',
+ $handles[$rule->getAuthorPHID()]->renderLink()));
+ } else if ($rule->isObjectRule()) {
+ $item->addIcon('fa-briefcase', pht('Object Rule'));
+ } else {
+ $item->addIcon('fa-globe', pht('Global Rule'));
+ }
+
+ if ($rule->getIsDisabled()) {
+ $item->setDisabled(true);
+ $item->addIcon('fa-lock grey', pht('Disabled'));
+ } else if (!$rule->hasValidAuthor()) {
+ $item->setDisabled(true);
+ $item->addIcon('fa-user grey', pht('Author Not Active'));
+ }
+
+ $content_type_name = idx($content_type_map, $rule->getContentType());
+ $item->addAttribute(pht('Affects: %s', $content_type_name));
+
+ $list->addItem($item);
+ }
+
+ return $list;
+ }
+
+}
diff --git a/src/applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php b/src/applications/herald/xaction/HeraldRuleDisableTransaction.php
similarity index 50%
copy from src/applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php
copy to src/applications/herald/xaction/HeraldRuleDisableTransaction.php
index df4f0feb0..5debab653 100644
--- a/src/applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php
+++ b/src/applications/herald/xaction/HeraldRuleDisableTransaction.php
@@ -1,32 +1,32 @@
<?php
-final class PhabricatorOwnersPackageAuditingTransaction
- extends PhabricatorOwnersPackageTransactionType {
+final class HeraldRuleDisableTransaction
+ extends HeraldRuleTransactionType {
- const TRANSACTIONTYPE = 'owners.auditing';
+ const TRANSACTIONTYPE = 'herald:disable';
public function generateOldValue($object) {
- return (int)$object->getAuditingEnabled();
+ return (bool)$object->getIsDisabled();
}
public function generateNewValue($object, $value) {
- return (int)$value;
+ return (bool)$value;
}
public function applyInternalEffects($object, $value) {
- $object->setAuditingEnabled($value);
+ $object->setIsDisabled((int)$value);
}
public function getTitle() {
if ($this->getNewValue()) {
return pht(
- '%s enabled auditing for this package.',
+ '%s disabled this rule.',
$this->renderAuthor());
} else {
return pht(
- '%s disabled auditing for this package.',
+ '%s enabled this rule.',
$this->renderAuthor());
}
}
}
diff --git a/src/applications/herald/xaction/HeraldRuleEditTransaction.php b/src/applications/herald/xaction/HeraldRuleEditTransaction.php
new file mode 100644
index 000000000..c4b03983f
--- /dev/null
+++ b/src/applications/herald/xaction/HeraldRuleEditTransaction.php
@@ -0,0 +1,56 @@
+<?php
+
+final class HeraldRuleEditTransaction
+ extends HeraldRuleTransactionType {
+
+ const TRANSACTIONTYPE = 'herald:edit';
+
+ public function generateOldValue($object) {
+ return id(new HeraldRuleSerializer())
+ ->serializeRule($object);
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $new_state = id(new HeraldRuleSerializer())
+ ->deserializeRuleComponents($value);
+
+ $object->setMustMatchAll((int)$new_state['match_all']);
+ $object->attachConditions($new_state['conditions']);
+ $object->attachActions($new_state['actions']);
+
+ $new_repetition = $new_state['repetition_policy'];
+ $object->setRepetitionPolicyStringConstant($new_repetition);
+ }
+
+ public function applyExternalEffects($object, $value) {
+ $object->saveConditions($object->getConditions());
+ $object->saveActions($object->getActions());
+ }
+
+ public function getTitle() {
+ return pht(
+ '%s edited this rule.',
+ $this->renderAuthor());
+ }
+
+ public function hasChangeDetailView() {
+ return true;
+ }
+
+ public function newChangeDetailView() {
+ $viewer = $this->getViewer();
+
+ $old = $this->getOldValue();
+ $new = $this->getNewValue();
+
+ $json = new PhutilJSON();
+ $old_json = $json->encodeFormatted($old);
+ $new_json = $json->encodeFormatted($new);
+
+ return id(new PhabricatorApplicationTransactionTextDiffDetailView())
+ ->setViewer($viewer)
+ ->setOldText($old_json)
+ ->setNewText($new_json);
+ }
+
+}
diff --git a/src/applications/herald/xaction/HeraldRuleNameTransaction.php b/src/applications/herald/xaction/HeraldRuleNameTransaction.php
new file mode 100644
index 000000000..39ce289d3
--- /dev/null
+++ b/src/applications/herald/xaction/HeraldRuleNameTransaction.php
@@ -0,0 +1,48 @@
+<?php
+
+final class HeraldRuleNameTransaction
+ extends HeraldRuleTransactionType {
+
+ const TRANSACTIONTYPE = 'herald:name';
+
+ public function generateOldValue($object) {
+ return $object->getName();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setName($value);
+ }
+
+ public function getTitle() {
+ return pht(
+ '%s renamed this rule 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('Rules must have a name.'));
+ }
+
+ $max_length = $object->getColumnMaximumByteLength('name');
+ foreach ($xactions as $xaction) {
+ $new_value = $xaction->getNewValue();
+
+ $new_length = strlen($new_value);
+ if ($new_length > $max_length) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Rule names can be no longer than %s characters.',
+ new PhutilNumber($max_length)));
+ }
+ }
+
+ return $errors;
+ }
+
+}
diff --git a/src/applications/herald/xaction/HeraldRuleTransactionType.php b/src/applications/herald/xaction/HeraldRuleTransactionType.php
new file mode 100644
index 000000000..81c6846b1
--- /dev/null
+++ b/src/applications/herald/xaction/HeraldRuleTransactionType.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class HeraldRuleTransactionType
+ extends PhabricatorModularTransactionType {}
diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignController.php b/src/applications/legalpad/controller/LegalpadDocumentSignController.php
index ab98c0bb7..fb15e2af8 100644
--- a/src/applications/legalpad/controller/LegalpadDocumentSignController.php
+++ b/src/applications/legalpad/controller/LegalpadDocumentSignController.php
@@ -1,706 +1,714 @@
<?php
final class LegalpadDocumentSignController extends LegalpadController {
+ private $isSessionGate;
+
public function shouldAllowPublic() {
return true;
}
public function shouldAllowLegallyNonCompliantUsers() {
return true;
}
+ public function setIsSessionGate($is_session_gate) {
+ $this->isSessionGate = $is_session_gate;
+ return $this;
+ }
+
+ public function getIsSessionGate() {
+ return $this->isSessionGate;
+ }
+
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$document = id(new LegalpadDocumentQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->needDocumentBodies(true)
->executeOne();
if (!$document) {
return new Aphront404Response();
}
$information = $this->readSignerInformation(
$document,
$request);
if ($information instanceof AphrontResponse) {
return $information;
}
list($signer_phid, $signature_data) = $information;
$signature = null;
$type_individual = LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL;
$is_individual = ($document->getSignatureType() == $type_individual);
switch ($document->getSignatureType()) {
case LegalpadDocument::SIGNATURE_TYPE_NONE:
// nothing to sign means this should be true
$has_signed = true;
// this is a status UI element
$signed_status = null;
break;
case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
if ($signer_phid) {
// TODO: This is odd and should probably be adjusted after
// grey/external accounts work better, but use the omnipotent
// viewer to check for a signature so we can pick up
// anonymous/grey signatures.
$signature = id(new LegalpadDocumentSignatureQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withDocumentPHIDs(array($document->getPHID()))
->withSignerPHIDs(array($signer_phid))
->executeOne();
if ($signature && !$viewer->isLoggedIn()) {
return $this->newDialog()
->setTitle(pht('Already Signed'))
->appendParagraph(pht('You have already signed this document!'))
->addCancelButton('/'.$document->getMonogram(), pht('Okay'));
}
}
$signed_status = null;
if (!$signature) {
$has_signed = false;
$signature = id(new LegalpadDocumentSignature())
->setSignerPHID($signer_phid)
->setDocumentPHID($document->getPHID())
->setDocumentVersion($document->getVersions());
// If the user is logged in, show a notice that they haven't signed.
// If they aren't logged in, we can't be as sure, so don't show
// anything.
if ($viewer->isLoggedIn()) {
$signed_status = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(
array(
pht('You have not signed this document yet.'),
));
}
} else {
$has_signed = true;
$signature_data = $signature->getSignatureData();
// In this case, we know they've signed.
$signed_at = $signature->getDateCreated();
if ($signature->getIsExemption()) {
$exemption_phid = $signature->getExemptionPHID();
$handles = $this->loadViewerHandles(array($exemption_phid));
$exemption_handle = $handles[$exemption_phid];
$signed_text = pht(
'You do not need to sign this document. '.
'%s added a signature exemption for you on %s.',
$exemption_handle->renderLink(),
phabricator_datetime($signed_at, $viewer));
} else {
$signed_text = pht(
'You signed this document on %s.',
phabricator_datetime($signed_at, $viewer));
}
$signed_status = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setErrors(array($signed_text));
}
$field_errors = array(
'name' => true,
'email' => true,
'agree' => true,
);
$signature->setSignatureData($signature_data);
break;
case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
$signature = id(new LegalpadDocumentSignature())
->setDocumentPHID($document->getPHID())
->setDocumentVersion($document->getVersions());
if ($viewer->isLoggedIn()) {
$has_signed = false;
$signed_status = null;
} else {
// This just hides the form.
$has_signed = true;
$login_text = pht(
'This document requires a corporate signatory. You must log in to '.
'accept this document on behalf of a company you represent.');
$signed_status = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(array($login_text));
}
$field_errors = array(
'name' => true,
'address' => true,
'contact.name' => true,
'email' => true,
);
$signature->setSignatureData($signature_data);
break;
}
$errors = array();
$hisec_token = null;
if ($request->isFormOrHisecPost() && !$has_signed) {
list($form_data, $errors, $field_errors) = $this->readSignatureForm(
$document,
$request);
$signature_data = $form_data + $signature_data;
$signature->setSignatureData($signature_data);
$signature->setSignatureType($document->getSignatureType());
$signature->setSignerName((string)idx($signature_data, 'name'));
$signature->setSignerEmail((string)idx($signature_data, 'email'));
$agree = $request->getExists('agree');
if (!$agree) {
$errors[] = pht(
'You must check "I agree to the terms laid forth above."');
$field_errors['agree'] = pht('Required');
}
if ($viewer->isLoggedIn() && $is_individual) {
$verified = LegalpadDocumentSignature::VERIFIED;
} else {
$verified = LegalpadDocumentSignature::UNVERIFIED;
}
$signature->setVerified($verified);
if (!$errors) {
// Require MFA to sign legal documents.
if ($viewer->isLoggedIn()) {
$workflow_key = sprintf(
'legalpad.sign(%s)',
$document->getPHID());
$hisec_token = id(new PhabricatorAuthSessionEngine())
->setWorkflowKey($workflow_key)
->requireHighSecurityToken(
$viewer,
$request,
$document->getURI());
}
$signature->save();
// If the viewer is logged in, signing for themselves, send them to
// the document page, which will show that they have signed the
// document. Unless of course they were required to sign the
// document to use Phabricator; in that case try really hard to
// re-direct them to where they wanted to go.
//
// Otherwise, send them to a completion page.
if ($viewer->isLoggedIn() && $is_individual) {
$next_uri = '/'.$document->getMonogram();
if ($document->getRequireSignature()) {
$request_uri = $request->getRequestURI();
$next_uri = (string)$request_uri;
}
} else {
$this->sendVerifySignatureEmail(
$document,
$signature);
$next_uri = $this->getApplicationURI('done/');
}
return id(new AphrontRedirectResponse())->setURI($next_uri);
}
}
$document_body = $document->getDocumentBody();
$engine = id(new PhabricatorMarkupEngine())
->setViewer($viewer);
$engine->addObject(
$document_body,
LegalpadDocumentBody::MARKUP_FIELD_TEXT);
$engine->process();
$document_markup = $engine->getOutput(
$document_body,
LegalpadDocumentBody::MARKUP_FIELD_TEXT);
$title = $document_body->getTitle();
$manage_uri = $this->getApplicationURI('view/'.$document->getID().'/');
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$document,
PhabricatorPolicyCapability::CAN_EDIT);
// Use the last content update as the modified date. We don't want to
// show that a document like a TOS was "updated" by an incidental change
// to a field like the preamble or privacy settings which does not actually
// affect the content of the agreement.
$content_updated = $document_body->getDateCreated();
// NOTE: We're avoiding `setPolicyObject()` here so we don't pick up
// extra UI elements that are unnecessary and clutter the signature page.
// These details are available on the "Manage" page.
$header = id(new PHUIHeaderView())
->setHeader($title)
->setUser($viewer)
- ->setEpoch($content_updated)
- ->addActionLink(
+ ->setEpoch($content_updated);
+
+ // If we're showing the user this document because it's required to use
+ // Phabricator and they haven't signed it, don't show the "Manage" button,
+ // since it won't work.
+ $is_gate = $this->getIsSessionGate();
+ if (!$is_gate) {
+ $header->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-pencil')
->setText(pht('Manage'))
->setHref($manage_uri)
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
+ }
$preamble_box = null;
if (strlen($document->getPreamble())) {
$preamble_text = new PHUIRemarkupView($viewer, $document->getPreamble());
// NOTE: We're avoiding `setObject()` here so we don't pick up extra UI
// elements like "Subscribers". This information is available on the
// "Manage" page, but just clutters up the "Signature" page.
$preamble = id(new PHUIPropertyListView())
->setUser($viewer)
->addSectionHeader(pht('Preamble'))
->addTextContent($preamble_text);
$preamble_box = new PHUIPropertyGroupView();
$preamble_box->addPropertyList($preamble);
}
$content = id(new PHUIDocumentView())
->addClass('legalpad')
->setHeader($header)
->appendChild(
array(
$signed_status,
$preamble_box,
$document_markup,
));
$signature_box = null;
if (!$has_signed) {
$error_view = null;
if ($errors) {
$error_view = id(new PHUIInfoView())
->setErrors($errors);
}
$signature_form = $this->buildSignatureForm(
$document,
$signature,
$field_errors);
switch ($document->getSignatureType()) {
default:
break;
case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
$box = id(new PHUIObjectBoxView())
->addClass('document-sign-box')
->setHeaderText(pht('Agree and Sign Document'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setForm($signature_form);
if ($error_view) {
$box->setInfoView($error_view);
}
$signature_box = phutil_tag_div(
'phui-document-view-pro-box plt', $box);
break;
}
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->setBorder(true);
$crumbs->addTextCrumb($document->getMonogram());
$box = id(new PHUITwoColumnView())
->setFooter($signature_box);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->setPageObjectPHIDs(array($document->getPHID()))
->appendChild(array(
$content,
$box,
));
}
private function readSignerInformation(
LegalpadDocument $document,
AphrontRequest $request) {
$viewer = $request->getUser();
$signer_phid = null;
$signature_data = array();
switch ($document->getSignatureType()) {
case LegalpadDocument::SIGNATURE_TYPE_NONE:
break;
case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
if ($viewer->isLoggedIn()) {
$signer_phid = $viewer->getPHID();
$signature_data = array(
'name' => $viewer->getRealName(),
'email' => $viewer->loadPrimaryEmailAddress(),
);
} else if ($request->isFormPost()) {
$email = new PhutilEmailAddress($request->getStr('email'));
if (strlen($email->getDomainName())) {
$email_obj = id(new PhabricatorUserEmail())
->loadOneWhere('address = %s', $email->getAddress());
if ($email_obj) {
return $this->signInResponse();
}
- $external_account = id(new PhabricatorExternalAccountQuery())
- ->setViewer($viewer)
- ->withAccountTypes(array('email'))
- ->withAccountDomains(array($email->getDomainName()))
- ->withAccountIDs(array($email->getAddress()))
- ->loadOneOrCreate();
- if ($external_account->getUserPHID()) {
- return $this->signInResponse();
- }
- $signer_phid = $external_account->getPHID();
}
}
break;
case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
$signer_phid = $viewer->getPHID();
if ($signer_phid) {
$signature_data = array(
'contact.name' => $viewer->getRealName(),
'email' => $viewer->loadPrimaryEmailAddress(),
'actorPHID' => $viewer->getPHID(),
);
}
break;
}
return array($signer_phid, $signature_data);
}
private function buildSignatureForm(
LegalpadDocument $document,
LegalpadDocumentSignature $signature,
array $errors) {
$viewer = $this->getRequest()->getUser();
$data = $signature->getSignatureData();
$form = id(new AphrontFormView())
->setUser($viewer);
$signature_type = $document->getSignatureType();
switch ($signature_type) {
case LegalpadDocument::SIGNATURE_TYPE_NONE:
// bail out of here quick
return;
case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
$this->buildIndividualSignatureForm(
$form,
$document,
$signature,
$errors);
break;
case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
$this->buildCorporateSignatureForm(
$form,
$document,
$signature,
$errors);
break;
default:
throw new Exception(
pht(
'This document has an unknown signature type ("%s").',
$signature_type));
}
$form
->appendChild(
id(new AphrontFormCheckboxControl())
->setError(idx($errors, 'agree', null))
->addCheckbox(
'agree',
'agree',
pht('I agree to the terms laid forth above.'),
false));
if ($document->getRequireSignature()) {
$cancel_uri = '/logout/';
$cancel_text = pht('Log Out');
} else {
$cancel_uri = $this->getApplicationURI();
$cancel_text = pht('Cancel');
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Sign Document'))
->addCancelButton($cancel_uri, $cancel_text));
return $form;
}
private function buildIndividualSignatureForm(
AphrontFormView $form,
LegalpadDocument $document,
LegalpadDocumentSignature $signature,
array $errors) {
$data = $signature->getSignatureData();
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Name'))
->setValue(idx($data, 'name', ''))
->setName('name')
->setError(idx($errors, 'name', null)));
$viewer = $this->getRequest()->getUser();
if (!$viewer->isLoggedIn()) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Email'))
->setValue(idx($data, 'email', ''))
->setName('email')
->setError(idx($errors, 'email', null)));
}
return $form;
}
private function buildCorporateSignatureForm(
AphrontFormView $form,
LegalpadDocument $document,
LegalpadDocumentSignature $signature,
array $errors) {
$data = $signature->getSignatureData();
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Company Name'))
->setValue(idx($data, 'name', ''))
->setName('name')
->setError(idx($errors, 'name', null)))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel(pht('Company Address'))
->setValue(idx($data, 'address', ''))
->setName('address')
->setError(idx($errors, 'address', null)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Contact Name'))
->setValue(idx($data, 'contact.name', ''))
->setName('contact.name')
->setError(idx($errors, 'contact.name', null)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Contact Email'))
->setValue(idx($data, 'email', ''))
->setName('email')
->setError(idx($errors, 'email', null)));
return $form;
}
private function readSignatureForm(
LegalpadDocument $document,
AphrontRequest $request) {
$signature_type = $document->getSignatureType();
switch ($signature_type) {
case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
$result = $this->readIndividualSignatureForm(
$document,
$request);
break;
case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
$result = $this->readCorporateSignatureForm(
$document,
$request);
break;
default:
throw new Exception(
pht(
'This document has an unknown signature type ("%s").',
$signature_type));
}
return $result;
}
private function readIndividualSignatureForm(
LegalpadDocument $document,
AphrontRequest $request) {
$signature_data = array();
$errors = array();
$field_errors = array();
$name = $request->getStr('name');
if (!strlen($name)) {
$field_errors['name'] = pht('Required');
$errors[] = pht('Name field is required.');
} else {
$field_errors['name'] = null;
}
$signature_data['name'] = $name;
$viewer = $request->getUser();
if ($viewer->isLoggedIn()) {
$email = $viewer->loadPrimaryEmailAddress();
} else {
$email = $request->getStr('email');
$addr_obj = null;
if (!strlen($email)) {
$field_errors['email'] = pht('Required');
$errors[] = pht('Email field is required.');
} else {
$addr_obj = new PhutilEmailAddress($email);
$domain = $addr_obj->getDomainName();
if (!$domain) {
$field_errors['email'] = pht('Invalid');
$errors[] = pht('A valid email is required.');
} else {
$field_errors['email'] = null;
}
}
}
$signature_data['email'] = $email;
return array($signature_data, $errors, $field_errors);
}
private function readCorporateSignatureForm(
LegalpadDocument $document,
AphrontRequest $request) {
$viewer = $request->getUser();
if (!$viewer->isLoggedIn()) {
throw new Exception(
pht(
'You can not sign a document on behalf of a corporation unless '.
'you are logged in.'));
}
$signature_data = array();
$errors = array();
$field_errors = array();
$name = $request->getStr('name');
if (!strlen($name)) {
$field_errors['name'] = pht('Required');
$errors[] = pht('Company name is required.');
} else {
$field_errors['name'] = null;
}
$signature_data['name'] = $name;
$address = $request->getStr('address');
if (!strlen($address)) {
$field_errors['address'] = pht('Required');
$errors[] = pht('Company address is required.');
} else {
$field_errors['address'] = null;
}
$signature_data['address'] = $address;
$contact_name = $request->getStr('contact.name');
if (!strlen($contact_name)) {
$field_errors['contact.name'] = pht('Required');
$errors[] = pht('Contact name is required.');
} else {
$field_errors['contact.name'] = null;
}
$signature_data['contact.name'] = $contact_name;
$email = $request->getStr('email');
$addr_obj = null;
if (!strlen($email)) {
$field_errors['email'] = pht('Required');
$errors[] = pht('Contact email is required.');
} else {
$addr_obj = new PhutilEmailAddress($email);
$domain = $addr_obj->getDomainName();
if (!$domain) {
$field_errors['email'] = pht('Invalid');
$errors[] = pht('A valid email is required.');
} else {
$field_errors['email'] = null;
}
}
$signature_data['email'] = $email;
return array($signature_data, $errors, $field_errors);
}
private function sendVerifySignatureEmail(
LegalpadDocument $doc,
LegalpadDocumentSignature $signature) {
$signature_data = $signature->getSignatureData();
$email = new PhutilEmailAddress($signature_data['email']);
$doc_name = $doc->getTitle();
$doc_link = PhabricatorEnv::getProductionURI('/'.$doc->getMonogram());
$path = $this->getApplicationURI(sprintf(
'/verify/%s/',
$signature->getSecretKey()));
$link = PhabricatorEnv::getProductionURI($path);
$name = idx($signature_data, 'name');
$body = pht(
"%s:\n\n".
"This email address was used to sign a Legalpad document ".
"in Phabricator:\n\n".
" %s\n\n".
"Please verify you own this email address and accept the ".
"agreement by clicking this link:\n\n".
" %s\n\n".
"Your signature is not valid until you complete this ".
"verification step.\n\nYou can review the document here:\n\n".
" %s\n",
$name,
$doc_name,
$link,
$doc_link);
id(new PhabricatorMetaMTAMail())
->addRawTos(array($email->getAddress()))
->setSubject(pht('[Legalpad] Signature Verification'))
->setForceDelivery(true)
->setBody($body)
->setRelatedPHID($signature->getDocumentPHID())
->saveAndSend();
}
private function signInResponse() {
return id(new Aphront403Response())
->setForbiddenText(
pht(
'The email address specified is associated with an account. '.
'Please login to that account and sign this document again.'));
}
}
diff --git a/src/applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php b/src/applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php
index 9df8d2478..ea14fd4a2 100644
--- a/src/applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php
+++ b/src/applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php
@@ -1,315 +1,315 @@
<?php
final class LegalpadDocumentSignatureSearchEngine
extends PhabricatorApplicationSearchEngine {
private $document;
public function getResultTypeDescription() {
return pht('Legalpad Signatures');
}
public function getApplicationClassName() {
return 'PhabricatorLegalpadApplication';
}
public function setDocument(LegalpadDocument $document) {
$this->document = $document;
return $this;
}
public function buildSavedQueryFromRequest(AphrontRequest $request) {
$saved = new PhabricatorSavedQuery();
$saved->setParameter(
'signerPHIDs',
$this->readUsersFromRequest($request, 'signers'));
$saved->setParameter(
'documentPHIDs',
$this->readPHIDsFromRequest(
$request,
'documents',
array(
PhabricatorLegalpadDocumentPHIDType::TYPECONST,
)));
$saved->setParameter('nameContains', $request->getStr('nameContains'));
$saved->setParameter('emailContains', $request->getStr('emailContains'));
return $saved;
}
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$query = id(new LegalpadDocumentSignatureQuery());
$signer_phids = $saved->getParameter('signerPHIDs', array());
if ($signer_phids) {
$query->withSignerPHIDs($signer_phids);
}
if ($this->document) {
$query->withDocumentPHIDs(array($this->document->getPHID()));
} else {
$document_phids = $saved->getParameter('documentPHIDs', array());
if ($document_phids) {
$query->withDocumentPHIDs($document_phids);
}
}
$name_contains = $saved->getParameter('nameContains');
if (strlen($name_contains)) {
$query->withNameContains($name_contains);
}
$email_contains = $saved->getParameter('emailContains');
if (strlen($email_contains)) {
$query->withEmailContains($email_contains);
}
return $query;
}
public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved_query) {
$document_phids = $saved_query->getParameter('documentPHIDs', array());
$signer_phids = $saved_query->getParameter('signerPHIDs', array());
if (!$this->document) {
$form
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(new LegalpadDocumentDatasource())
->setName('documents')
->setLabel(pht('Documents'))
->setValue($document_phids));
}
$name_contains = $saved_query->getParameter('nameContains', '');
$email_contains = $saved_query->getParameter('emailContains', '');
$form
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorPeopleDatasource())
->setName('signers')
->setLabel(pht('Signers'))
->setValue($signer_phids))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Name Contains'))
->setName('nameContains')
->setValue($name_contains))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Email Contains'))
->setName('emailContains')
->setValue($email_contains));
}
protected function getURI($path) {
if ($this->document) {
return '/legalpad/signatures/'.$this->document->getID().'/'.$path;
} else {
return '/legalpad/signatures/'.$path;
}
}
protected function getBuiltinQueryNames() {
$names = array(
'all' => pht('All Signatures'),
);
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 getRequiredHandlePHIDsForResultList(
array $signatures,
PhabricatorSavedQuery $query) {
return array_merge(
mpull($signatures, 'getSignerPHID'),
mpull($signatures, 'getDocumentPHID'));
}
protected function renderResultList(
array $signatures,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($signatures, 'LegalpadDocumentSignature');
$viewer = $this->requireViewer();
Javelin::initBehavior('phabricator-tooltips');
$sig_good = $this->renderIcon(
'fa-check',
null,
pht('Verified, Current'));
$sig_corp = $this->renderIcon(
'fa-building-o',
null,
pht('Verified, Corporate'));
$sig_old = $this->renderIcon(
'fa-clock-o',
'orange',
pht('Signed Older Version'));
$sig_unverified = $this->renderIcon(
'fa-envelope',
'red',
pht('Unverified Email'));
$sig_exemption = $this->renderIcon(
'fa-asterisk',
'indigo',
pht('Exemption'));
id(new PHUIIconView())
->setIcon('fa-envelope', 'red')
->addSigil('has-tooltip')
->setMetadata(array('tip' => pht('Unverified Email')));
$type_corporate = LegalpadDocument::SIGNATURE_TYPE_CORPORATION;
$rows = array();
foreach ($signatures as $signature) {
$name = $signature->getSignerName();
$email = $signature->getSignerEmail();
$document = $signature->getDocument();
if ($signature->getIsExemption()) {
$sig_icon = $sig_exemption;
} else if (!$signature->isVerified()) {
$sig_icon = $sig_unverified;
} else if ($signature->getDocumentVersion() != $document->getVersions()) {
$sig_icon = $sig_old;
} else if ($signature->getSignatureType() == $type_corporate) {
$sig_icon = $sig_corp;
} else {
$sig_icon = $sig_good;
}
$signature_href = $this->getApplicationURI(
'signature/'.$signature->getID().'/');
$sig_icon = javelin_tag(
'a',
array(
'href' => $signature_href,
'sigil' => 'workflow',
),
$sig_icon);
$signer_phid = $signature->getSignerPHID();
$rows[] = array(
$sig_icon,
$handles[$document->getPHID()]->renderLink(),
$signer_phid
? $handles[$signer_phid]->renderLink()
- : null,
+ : phutil_tag('em', array(), pht('None')),
$name,
phutil_tag(
'a',
array(
'href' => 'mailto:'.$email,
),
$email),
phabricator_datetime($signature->getDateCreated(), $viewer),
);
}
$table = id(new AphrontTableView($rows))
->setNoDataString(pht('No signatures match the query.'))
->setHeaders(
array(
'',
pht('Document'),
pht('Account'),
pht('Name'),
pht('Email'),
pht('Signed'),
))
->setColumnVisibility(
array(
true,
// Only show the "Document" column if we aren't scoped to a
// particular document.
!$this->document,
))
->setColumnClasses(
array(
'',
'',
'',
'',
'wide',
'right',
));
$button = null;
if ($this->document) {
$document_id = $this->document->getID();
$button = id(new PHUIButtonView())
->setText(pht('Add Exemption'))
->setTag('a')
->setHref($this->getApplicationURI('addsignature/'.$document_id.'/'))
->setWorkflow(true)
->setIcon('fa-pencil');
}
if (!$this->document) {
$table->setNotice(
pht('NOTE: You can only see your own signatures and signatures on '.
'documents you have permission to edit.'));
}
$result = new PhabricatorApplicationSearchResultView();
$result->setTable($table);
if ($button) {
$result->addAction($button);
}
return $result;
}
private function renderIcon($icon, $color, $title) {
Javelin::initBehavior('phabricator-tooltips');
return array(
id(new PHUIIconView())
->setIcon($icon, $color)
->addSigil('has-tooltip')
->setMetadata(array('tip' => $title)),
javelin_tag(
'span',
array(
'aural' => true,
),
$title),
);
}
}
diff --git a/src/applications/legalpad/xaction/LegalpadDocumentRequireSignatureTransaction.php b/src/applications/legalpad/xaction/LegalpadDocumentRequireSignatureTransaction.php
index 3819f38a7..9932baab8 100644
--- a/src/applications/legalpad/xaction/LegalpadDocumentRequireSignatureTransaction.php
+++ b/src/applications/legalpad/xaction/LegalpadDocumentRequireSignatureTransaction.php
@@ -1,72 +1,83 @@
<?php
final class LegalpadDocumentRequireSignatureTransaction
extends LegalpadDocumentTransactionType {
const TRANSACTIONTYPE = 'legalpad:require-signature';
public function generateOldValue($object) {
return (int)$object->getRequireSignature();
}
public function applyInternalEffects($object, $value) {
$object->setRequireSignature((int)$value);
}
public function applyExternalEffects($object, $value) {
if ($value) {
$session = new PhabricatorAuthSession();
queryfx(
$session->establishConnection('w'),
'UPDATE %T SET signedLegalpadDocuments = 0',
$session->getTableName());
}
}
public function getTitle() {
$new = $this->getNewValue();
if ($new) {
return pht(
'%s set the document to require signatures.',
$this->renderAuthor());
} else {
return pht(
'%s set the document to not require signatures.',
$this->renderAuthor());
}
}
public function getTitleForFeed() {
$new = $this->getNewValue();
if ($new) {
return pht(
'%s set the document %s to require signatures.',
$this->renderAuthor(),
$this->renderObject());
} else {
return pht(
'%s set the document %s to not require signatures.',
$this->renderAuthor(),
$this->renderObject());
}
}
public function validateTransactions($object, array $xactions) {
$errors = array();
- $is_admin = $this->getActor()->getIsAdmin();
+ $old = (bool)$object->getRequireSignature();
+ foreach ($xactions as $xaction) {
+ $new = (bool)$xaction->getNewValue();
- if (!$is_admin) {
- $errors[] = $this->newInvalidError(
- pht('Only admins may require signature.'));
+ if ($old === $new) {
+ continue;
+ }
+
+ $is_admin = $this->getActor()->getIsAdmin();
+ if (!$is_admin) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Only administrators may change whether a document '.
+ 'requires a signature.'),
+ $xaction);
+ }
}
return $errors;
}
public function getIcon() {
return 'fa-pencil-square';
}
}
diff --git a/src/applications/macro/engine/PhabricatorMemeEngine.php b/src/applications/macro/engine/PhabricatorMemeEngine.php
index 7433a4e8b..afee0f9b1 100644
--- a/src/applications/macro/engine/PhabricatorMemeEngine.php
+++ b/src/applications/macro/engine/PhabricatorMemeEngine.php
@@ -1,399 +1,402 @@
<?php
final class PhabricatorMemeEngine extends Phobject {
private $viewer;
private $template;
private $aboveText;
private $belowText;
private $templateFile;
private $metrics;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setTemplate($template) {
$this->template = $template;
return $this;
}
public function getTemplate() {
return $this->template;
}
public function setAboveText($above_text) {
$this->aboveText = $above_text;
return $this;
}
public function getAboveText() {
return $this->aboveText;
}
public function setBelowText($below_text) {
$this->belowText = $below_text;
return $this;
}
public function getBelowText() {
return $this->belowText;
}
public function getGenerateURI() {
- return id(new PhutilURI('/macro/meme/'))
- ->alter('macro', $this->getTemplate())
- ->alter('above', $this->getAboveText())
- ->alter('below', $this->getBelowText());
+ $params = array(
+ 'macro' => $this->getTemplate(),
+ 'above' => $this->getAboveText(),
+ 'below' => $this->getBelowText(),
+ );
+
+ return new PhutilURI('/macro/meme/', $params);
}
public function newAsset() {
$cache = $this->loadCachedFile();
if ($cache) {
return $cache;
}
$template = $this->loadTemplateFile();
if (!$template) {
throw new Exception(
pht(
'Template "%s" is not a valid template.',
$template));
}
$hash = $this->newTransformHash();
$asset = $this->newAssetFile($template);
$xfile = id(new PhabricatorTransformedFile())
->setOriginalPHID($template->getPHID())
->setTransformedPHID($asset->getPHID())
->setTransform($hash);
try {
$caught = null;
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
try {
$xfile->save();
} catch (Exception $ex) {
$caught = $ex;
}
unset($unguarded);
if ($caught) {
throw $caught;
}
return $asset;
} catch (AphrontDuplicateKeyQueryException $ex) {
$xfile = $this->loadCachedFile();
if (!$xfile) {
throw $ex;
}
return $xfile;
}
}
private function newTransformHash() {
$properties = array(
'kind' => 'meme',
'above' => $this->getAboveText(),
'below' => $this->getBelowText(),
);
$properties = phutil_json_encode($properties);
return PhabricatorHash::digestForIndex($properties);
}
public function loadCachedFile() {
$viewer = $this->getViewer();
$template_file = $this->loadTemplateFile();
if (!$template_file) {
return null;
}
$hash = $this->newTransformHash();
$xform = id(new PhabricatorTransformedFile())->loadOneWhere(
'originalPHID = %s AND transform = %s',
$template_file->getPHID(),
$hash);
if (!$xform) {
return null;
}
return id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($xform->getTransformedPHID()))
->executeOne();
}
private function loadTemplateFile() {
if ($this->templateFile === null) {
$viewer = $this->getViewer();
$template = $this->getTemplate();
$macro = id(new PhabricatorMacroQuery())
->setViewer($viewer)
->withNames(array($template))
->needFiles(true)
->executeOne();
if (!$macro) {
return null;
}
$this->templateFile = $macro->getFile();
}
return $this->templateFile;
}
private function newAssetFile(PhabricatorFile $template) {
$data = $this->newAssetData($template);
return PhabricatorFile::newFromFileData(
$data,
array(
'name' => 'meme-'.$template->getName(),
'canCDN' => true,
// In modern code these can end up linked directly in email, so let
// them stick around for a while.
'ttl.relative' => phutil_units('30 days in seconds'),
));
}
private function newAssetData(PhabricatorFile $template) {
$template_data = $template->loadFileData();
// When we aren't adding text, just return the data unmodified. This saves
// us from doing expensive stitching when we aren't actually making any
// changes to the image.
$above_text = $this->getAboveText();
$below_text = $this->getBelowText();
if (!strlen(trim($above_text)) && !strlen(trim($below_text))) {
return $template_data;
}
$result = $this->newImagemagickAsset($template, $template_data);
if ($result) {
return $result;
}
return $this->newGDAsset($template, $template_data);
}
private function newImagemagickAsset(
PhabricatorFile $template,
$template_data) {
// We're only going to use Imagemagick on GIFs.
$mime_type = $template->getMimeType();
if ($mime_type != 'image/gif') {
return null;
}
// We're only going to use Imagemagick if it is actually available.
$available = PhabricatorEnv::getEnvConfig('files.enable-imagemagick');
if (!$available) {
return null;
}
// Test of the GIF is an animated GIF. If it's a flat GIF, we'll fall
// back to GD.
$input = new TempFile();
Filesystem::writeFile($input, $template_data);
list($err, $out) = exec_manual('convert %s info:', $input);
if ($err) {
return null;
}
$split = phutil_split_lines($out);
$frames = count($split);
if ($frames <= 1) {
return null;
}
// Split the frames apart, transform each frame, then merge them back
// together.
$output = new TempFile();
$future = new ExecFuture(
'convert %s -coalesce +adjoin %s_%s',
$input,
$input,
'%09d');
$future->setTimeout(10)->resolvex();
$output_files = array();
for ($ii = 0; $ii < $frames; $ii++) {
$frame_name = sprintf('%s_%09d', $input, $ii);
$output_name = sprintf('%s_%09d', $output, $ii);
$output_files[] = $output_name;
$frame_data = Filesystem::readFile($frame_name);
$memed_frame_data = $this->newGDAsset($template, $frame_data);
Filesystem::writeFile($output_name, $memed_frame_data);
}
$future = new ExecFuture(
'convert -dispose background -loop 0 %Ls %s',
$output_files,
$output);
$future->setTimeout(10)->resolvex();
return Filesystem::readFile($output);
}
private function newGDAsset(PhabricatorFile $template, $data) {
$img = imagecreatefromstring($data);
if (!$img) {
throw new Exception(
pht('Failed to imagecreatefromstring() image template data.'));
}
$dx = imagesx($img);
$dy = imagesy($img);
$metrics = $this->getMetrics($dx, $dy);
$font = $this->getFont();
$size = $metrics['size'];
$above = $this->getAboveText();
if (strlen($above)) {
$x = (int)floor(($dx - $metrics['text']['above']['width']) / 2);
$y = $metrics['text']['above']['height'] + 12;
$this->drawText($img, $font, $metrics['size'], $x, $y, $above);
}
$below = $this->getBelowText();
if (strlen($below)) {
$x = (int)floor(($dx - $metrics['text']['below']['width']) / 2);
$y = $dy - 12 - $metrics['text']['below']['descend'];
$this->drawText($img, $font, $metrics['size'], $x, $y, $below);
}
return PhabricatorImageTransformer::saveImageDataInAnyFormat(
$img,
$template->getMimeType());
}
private function getFont() {
$phabricator_root = dirname(phutil_get_library_root('phabricator'));
$font_root = $phabricator_root.'/resources/font/';
if (Filesystem::pathExists($font_root.'impact.ttf')) {
$font_path = $font_root.'impact.ttf';
} else {
$font_path = $font_root.'tuffy.ttf';
}
return $font_path;
}
private function getMetrics($dim_x, $dim_y) {
if ($this->metrics === null) {
$font = $this->getFont();
$font_max = 72;
$font_min = 5;
$margin_x = 16;
$margin_y = 16;
$last = null;
$cursor = floor(($font_max + $font_min) / 2);
$min = $font_min;
$max = $font_max;
$texts = array(
'above' => $this->getAboveText(),
'below' => $this->getBelowText(),
);
$metrics = null;
$best = null;
while (true) {
$all_fit = true;
$text_metrics = array();
foreach ($texts as $key => $text) {
$box = imagettfbbox($cursor, 0, $font, $text);
$height = abs($box[3] - $box[5]);
$width = abs($box[0] - $box[2]);
// This is the number of pixels below the baseline that the
// text extends, for example if it has a "y".
$descend = $box[3];
if (($height + $margin_y) > $dim_y) {
$all_fit = false;
break;
}
if (($width + $margin_x) > $dim_x) {
$all_fit = false;
break;
}
$text_metrics[$key]['width'] = $width;
$text_metrics[$key]['height'] = $height;
$text_metrics[$key]['descend'] = $descend;
}
if ($all_fit || $best === null) {
$best = $cursor;
$metrics = $text_metrics;
}
if ($all_fit) {
$min = $cursor;
} else {
$max = $cursor;
}
$last = $cursor;
$cursor = floor(($max + $min) / 2);
if ($cursor === $last) {
break;
}
}
$this->metrics = array(
'size' => $best,
'text' => $metrics,
);
}
return $this->metrics;
}
private function drawText($img, $font, $size, $x, $y, $text) {
$text_color = imagecolorallocate($img, 255, 255, 255);
$border_color = imagecolorallocate($img, 0, 0, 0);
$border = 2;
for ($xx = ($x - $border); $xx <= ($x + $border); $xx += $border) {
for ($yy = ($y - $border); $yy <= ($y + $border); $yy += $border) {
if (($xx === $x) && ($yy === $y)) {
continue;
}
imagettftext($img, $size, 0, $xx, $yy, $border_color, $font, $text);
}
}
imagettftext($img, $size, 0, $x, $y, $text_color, $font, $text);
}
}
diff --git a/src/applications/macro/query/PhabricatorMacroQuery.php b/src/applications/macro/query/PhabricatorMacroQuery.php
index 3ba30502d..7635b68b7 100644
--- a/src/applications/macro/query/PhabricatorMacroQuery.php
+++ b/src/applications/macro/query/PhabricatorMacroQuery.php
@@ -1,269 +1,268 @@
<?php
final class PhabricatorMacroQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $authorPHIDs;
private $names;
private $nameLike;
private $namePrefix;
private $dateCreatedAfter;
private $dateCreatedBefore;
private $flagColor;
private $needFiles;
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_ACTIVE = 'status-active';
const STATUS_DISABLED = 'status-disabled';
public static function getStatusOptions() {
return array(
self::STATUS_ACTIVE => pht('Active Macros'),
self::STATUS_DISABLED => pht('Disabled Macros'),
self::STATUS_ANY => pht('Active and Disabled Macros'),
);
}
public static function getFlagColorsOptions() {
$options = array(
'-1' => pht('(No Filtering)'),
'-2' => pht('(Marked With Any Flag)'),
);
foreach (PhabricatorFlagColor::getColorNameMap() as $color => $name) {
$options[$color] = $name;
}
return $options;
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAuthorPHIDs(array $author_phids) {
$this->authorPHIDs = $author_phids;
return $this;
}
public function withNameLike($name) {
$this->nameLike = $name;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withNamePrefix($prefix) {
$this->namePrefix = $prefix;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withDateCreatedBefore($date_created_before) {
$this->dateCreatedBefore = $date_created_before;
return $this;
}
public function withDateCreatedAfter($date_created_after) {
$this->dateCreatedAfter = $date_created_after;
return $this;
}
public function withFlagColor($flag_color) {
$this->flagColor = $flag_color;
return $this;
}
public function needFiles($need_files) {
$this->needFiles = $need_files;
return $this;
}
public function newResultObject() {
return new PhabricatorFileImageMacro();
}
protected function loadPage() {
return $this->loadStandardPage(new PhabricatorFileImageMacro());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'm.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'm.phid IN (%Ls)',
$this->phids);
}
if ($this->authorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'm.authorPHID IN (%Ls)',
$this->authorPHIDs);
}
if (strlen($this->nameLike)) {
$where[] = qsprintf(
$conn,
'm.name LIKE %~',
$this->nameLike);
}
if ($this->names !== null) {
$where[] = qsprintf(
$conn,
'm.name IN (%Ls)',
$this->names);
}
if (strlen($this->namePrefix)) {
$where[] = qsprintf(
$conn,
'm.name LIKE %>',
$this->namePrefix);
}
switch ($this->status) {
case self::STATUS_ACTIVE:
$where[] = qsprintf(
$conn,
'm.isDisabled = 0');
break;
case self::STATUS_DISABLED:
$where[] = qsprintf(
$conn,
'm.isDisabled = 1');
break;
case self::STATUS_ANY:
break;
default:
throw new Exception(pht("Unknown status '%s'!", $this->status));
}
if ($this->dateCreatedAfter) {
$where[] = qsprintf(
$conn,
'm.dateCreated >= %d',
$this->dateCreatedAfter);
}
if ($this->dateCreatedBefore) {
$where[] = qsprintf(
$conn,
'm.dateCreated <= %d',
$this->dateCreatedBefore);
}
if ($this->flagColor != '-1' && $this->flagColor !== null) {
if ($this->flagColor == '-2') {
$flag_colors = array_keys(PhabricatorFlagColor::getColorNameMap());
} else {
$flag_colors = array($this->flagColor);
}
$flags = id(new PhabricatorFlagQuery())
->withOwnerPHIDs(array($this->getViewer()->getPHID()))
->withTypes(array(PhabricatorMacroMacroPHIDType::TYPECONST))
->withColors($flag_colors)
->setViewer($this->getViewer())
->execute();
if (empty($flags)) {
throw new PhabricatorEmptyQueryException(pht('No matching flags.'));
} else {
$where[] = qsprintf(
$conn,
'm.phid IN (%Ls)',
mpull($flags, 'getObjectPHID'));
}
}
return $where;
}
protected function didFilterPage(array $macros) {
if ($this->needFiles) {
$file_phids = mpull($macros, 'getFilePHID');
$files = id(new PhabricatorFileQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
foreach ($macros as $key => $macro) {
$file = idx($files, $macro->getFilePHID());
if (!$file) {
unset($macros[$key]);
continue;
}
$macro->attachFile($file);
}
}
return $macros;
}
protected function getPrimaryTableAlias() {
return 'm';
}
public function getQueryApplicationClass() {
return 'PhabricatorMacroApplication';
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => 'm',
'column' => 'name',
'type' => 'string',
'reverse' => true,
'unique' => true,
),
);
}
- protected function getPagingValueMap($cursor, array $keys) {
- $macro = $this->loadCursorObject($cursor);
+ protected function newPagingMapFromPartialObject($object) {
return array(
- 'id' => $macro->getID(),
- 'name' => $macro->getName(),
+ 'id' => (int)$object->getID(),
+ 'name' => $object->getName(),
);
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Name'),
),
) + parent::getBuiltinOrders();
}
}
diff --git a/src/applications/maniphest/application/PhabricatorManiphestApplication.php b/src/applications/maniphest/application/PhabricatorManiphestApplication.php
index 35c1efb6e..8ed20416b 100644
--- a/src/applications/maniphest/application/PhabricatorManiphestApplication.php
+++ b/src/applications/maniphest/application/PhabricatorManiphestApplication.php
@@ -1,113 +1,113 @@
<?php
final class PhabricatorManiphestApplication extends PhabricatorApplication {
public function getName() {
return pht('Maniphest');
}
public function getShortDescription() {
return pht('Tasks and Bugs');
}
public function getBaseURI() {
return '/maniphest/';
}
public function getIcon() {
return 'fa-anchor';
}
public function getTitleGlyph() {
return "\xE2\x9A\x93";
}
public function isPinnedByDefault(PhabricatorUser $viewer) {
return true;
}
public function getApplicationOrder() {
return 0.110;
}
public function getFactObjectsForAnalysis() {
return array(
new ManiphestTask(),
);
}
public function getRemarkupRules() {
return array(
new ManiphestRemarkupRule(),
);
}
public function getRoutes() {
return array(
'/T(?P<id>[1-9]\d*)' => 'ManiphestTaskDetailController',
'/maniphest/' => array(
$this->getQueryRoutePattern() => 'ManiphestTaskListController',
'report/(?:(?P<view>\w+)/)?' => 'ManiphestReportController',
$this->getBulkRoutePattern('bulk/') => 'ManiphestBulkEditController',
'task/' => array(
$this->getEditRoutePattern('edit/')
=> 'ManiphestTaskEditController',
'subtask/(?P<id>[1-9]\d*)/' => 'ManiphestTaskSubtaskController',
),
- 'subpriority/' => 'ManiphestSubpriorityController',
+ 'graph/(?P<id>[1-9]\d*)/' => 'ManiphestTaskGraphController',
),
);
}
public function supportsEmailIntegration() {
return true;
}
public function getAppEmailBlurb() {
return pht(
'Send email to these addresses to create tasks. %s',
phutil_tag(
'a',
array(
'href' => $this->getInboundEmailSupportLink(),
),
pht('Learn More')));
}
protected function getCustomCapabilities() {
return array(
ManiphestDefaultViewCapability::CAPABILITY => array(
'caption' => pht('Default view policy for newly created tasks.'),
'template' => ManiphestTaskPHIDType::TYPECONST,
'capability' => PhabricatorPolicyCapability::CAN_VIEW,
),
ManiphestDefaultEditCapability::CAPABILITY => array(
'caption' => pht('Default edit policy for newly created tasks.'),
'template' => ManiphestTaskPHIDType::TYPECONST,
'capability' => PhabricatorPolicyCapability::CAN_EDIT,
),
ManiphestBulkEditCapability::CAPABILITY => array(),
);
}
public function getMailCommandObjects() {
return array(
'task' => array(
'name' => pht('Email Commands: Tasks'),
'header' => pht('Interacting with Maniphest Tasks'),
'object' => new ManiphestTask(),
'summary' => pht(
'This page documents the commands you can use to interact with '.
'tasks in Maniphest. These commands work when creating new tasks '.
'via email and when replying to existing tasks.'),
),
);
}
public function getApplicationSearchDocumentTypes() {
return array(
ManiphestTaskPHIDType::TYPECONST,
);
}
}
diff --git a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php
index 8f2830908..f1916cffe 100644
--- a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php
+++ b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php
@@ -1,474 +1,498 @@
<?php
final class PhabricatorManiphestConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Maniphest');
}
public function getDescription() {
return pht('Configure Maniphest.');
}
public function getIcon() {
return 'fa-anchor';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
$priority_type = 'maniphest.priorities';
$priority_defaults = array(
100 => array(
'name' => pht('Unbreak Now!'),
'keywords' => array('unbreak'),
'short' => pht('Unbreak!'),
'color' => 'pink',
),
90 => array(
'name' => pht('Needs Triage'),
'keywords' => array('triage'),
'short' => pht('Triage'),
'color' => 'violet',
),
80 => array(
'name' => pht('High'),
'keywords' => array('high'),
'short' => pht('High'),
'color' => 'red',
),
50 => array(
'name' => pht('Normal'),
'keywords' => array('normal'),
'short' => pht('Normal'),
'color' => 'orange',
),
25 => array(
'name' => pht('Low'),
'keywords' => array('low'),
'short' => pht('Low'),
'color' => 'yellow',
),
0 => array(
'name' => pht('Wishlist'),
'keywords' => array('wish', 'wishlist'),
'short' => pht('Wish'),
'color' => 'sky',
),
);
$status_type = 'maniphest.statuses';
$status_defaults = array(
'open' => array(
'name' => pht('Open'),
'special' => ManiphestTaskStatus::SPECIAL_DEFAULT,
'prefixes' => array(
'open',
'opens',
'reopen',
'reopens',
),
),
'resolved' => array(
'name' => pht('Resolved'),
'name.full' => pht('Closed, Resolved'),
'closed' => true,
'special' => ManiphestTaskStatus::SPECIAL_CLOSED,
'transaction.icon' => 'fa-check-circle',
'prefixes' => array(
'closed',
'closes',
'close',
'fix',
'fixes',
'fixed',
'resolve',
'resolves',
'resolved',
),
'suffixes' => array(
'as resolved',
'as fixed',
),
'keywords' => array('closed', 'fixed', 'resolved'),
),
'wontfix' => array(
'name' => pht('Wontfix'),
'name.full' => pht('Closed, Wontfix'),
'transaction.icon' => 'fa-ban',
'closed' => true,
'prefixes' => array(
'wontfix',
'wontfixes',
'wontfixed',
),
'suffixes' => array(
'as wontfix',
),
),
'invalid' => array(
'name' => pht('Invalid'),
'name.full' => pht('Closed, Invalid'),
'transaction.icon' => 'fa-minus-circle',
'closed' => true,
'claim' => false,
'prefixes' => array(
'invalidate',
'invalidates',
'invalidated',
),
'suffixes' => array(
'as invalid',
),
),
'duplicate' => array(
'name' => pht('Duplicate'),
'name.full' => pht('Closed, Duplicate'),
'transaction.icon' => 'fa-files-o',
'special' => ManiphestTaskStatus::SPECIAL_DUPLICATE,
'closed' => true,
'claim' => false,
),
'spite' => array(
'name' => pht('Spite'),
'name.full' => pht('Closed, Spite'),
'name.action' => pht('Spited'),
'transaction.icon' => 'fa-thumbs-o-down',
'silly' => true,
'closed' => true,
'prefixes' => array(
'spite',
'spites',
'spited',
),
'suffixes' => array(
'out of spite',
'as spite',
),
),
);
$status_description = $this->deformat(pht(<<<EOTEXT
Allows you to edit, add, or remove the task statuses available in Maniphest,
like "Open", "Resolved" and "Invalid". The configuration should contain a map
of status constants to status specifications (see defaults below for examples).
The constant for each status should be 1-12 characters long and contain only
lowercase letters and digits. Valid examples are "open", "closed", and
"invalid". Users will not normally see these values.
The keys you can provide in a specification are:
- `name` //Required string.// Name of the status, like "Invalid".
- `name.full` //Optional string.// Longer name, like "Closed, Invalid". This
appears on the task detail view in the header.
- `name.action` //Optional string.// Action name for email subjects, like
"Marked Invalid".
- `closed` //Optional bool.// Statuses are either "open" or "closed".
Specifying `true` here will mark the status as closed (like "Resolved" or
"Invalid"). By default, statuses are open.
- `special` //Optional string.// Mark this status as special. The special
statuses are:
- `default` This is the default status for newly created tasks. You must
designate one status as default, and it must be an open status.
- `closed` This is the default status for closed tasks (for example, tasks
closed via the "!close" action in email or via the quick close button in
Maniphest). You must designate one status as the default closed status,
and it must be a closed status.
- `duplicate` This is the status used when tasks are merged into one
another as duplicates. You must designate one status for duplicates,
and it must be a closed status.
- `transaction.icon` //Optional string.// Allows you to choose a different
icon to use for this status when showing status changes in the transaction
log. Please see UIExamples, Icons and Images for a list.
- `transaction.color` //Optional string.// Allows you to choose a different
color to use for this status when showing status changes in the transaction
log.
- `silly` //Optional bool.// Marks this status as silly, and thus wholly
inappropriate for use by serious businesses.
- `prefixes` //Optional list<string>.// Allows you to specify a list of
text prefixes which will trigger a task transition into this status
when mentioned in a commit message. For example, providing "closes" here
will allow users to move tasks to this status by writing `Closes T123` in
commit messages.
- `suffixes` //Optional list<string>.// Allows you to specify a list of
text suffixes which will trigger a task transition into this status
when mentioned in a commit message, after a valid prefix. For example,
providing "as invalid" here will allow users to move tasks
to this status by writing `Closes T123 as invalid`, even if another status
is selected by the "Closes" prefix.
- `keywords` //Optional list<string>.// Allows you to specify a list
of keywords which can be used with `!status` commands in email to select
this status.
- `disabled` //Optional bool.// Marks this status as no longer in use so
tasks can not be created or edited to have this status. Existing tasks with
this status will not be affected, but you can batch edit them or let them
die out on their own.
- `claim` //Optional bool.// By default, closing an unassigned task claims
it. You can set this to `false` to disable this behavior for a particular
status.
- - `locked` //Optional bool.// Lock tasks in this status, preventing users
- from commenting.
+ - `locked` //Optional string.// Lock tasks in this status. Specify "comments"
+ to lock comments (users who can edit the task may override this lock).
+ Specify "edits" to prevent anyone except the task owner from making edits.
- `mfa` //Optional bool.// Require all edits to this task to be signed with
multi-factor authentication.
Statuses will appear in the UI in the order specified. Note the status marked
`special` as `duplicate` is not settable directly and will not appear in UI
elements, and that any status marked `silly` does not appear if Phabricator
is configured with `phabricator.serious-business` set to true.
Examining the default configuration and examples below will probably be helpful
in understanding these options.
EOTEXT
));
$status_example = array(
'open' => array(
'name' => pht('Open'),
'special' => 'default',
),
'closed' => array(
'name' => pht('Closed'),
'special' => 'closed',
'closed' => true,
),
'duplicate' => array(
'name' => pht('Duplicate'),
'special' => 'duplicate',
'closed' => true,
),
);
$json = new PhutilJSON();
$status_example = $json->encodeFormatted($status_example);
// This is intentionally blank for now, until we can move more Maniphest
// logic to custom fields.
$default_fields = array();
foreach ($default_fields as $key => $enabled) {
$default_fields[$key] = array(
'disabled' => !$enabled,
);
}
$custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType';
$fields_example = array(
'mycompany.estimated-hours' => array(
'name' => pht('Estimated Hours'),
'type' => 'int',
'caption' => pht('Estimated number of hours this will take.'),
),
);
$fields_json = id(new PhutilJSON())->encodeFormatted($fields_example);
$points_type = 'maniphest.points';
$points_example_1 = array(
'enabled' => true,
'label' => pht('Story Points'),
'action' => pht('Change Story Points'),
);
$points_json_1 = id(new PhutilJSON())->encodeFormatted($points_example_1);
$points_example_2 = array(
'enabled' => true,
'label' => pht('Estimated Hours'),
'action' => pht('Change Estimate'),
);
$points_json_2 = id(new PhutilJSON())->encodeFormatted($points_example_2);
$points_description = $this->deformat(pht(<<<EOTEXT
Activates a points field on tasks. You can use points for estimation or
planning. If configured, points will appear on workboards.
To activate points, set this value to a map with these keys:
- `enabled` //Optional bool.// Use `true` to enable points, or
`false` to disable them.
- `label` //Optional string.// Label for points, like "Story Points" or
"Estimated Hours". If omitted, points will be called "Points".
- `action` //Optional string.// Label for the action which changes points
in Maniphest, like "Change Estimate". If omitted, the action will
be called "Change Points".
See the example below for a starting point.
EOTEXT
));
$subtype_type = 'maniphest.subtypes';
$subtype_default_key = PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT;
$subtype_example = array(
array(
'key' => $subtype_default_key,
'name' => pht('Task'),
),
array(
'key' => 'bug',
'name' => pht('Bug'),
),
array(
'key' => 'feature',
'name' => pht('Feature Request'),
),
);
$subtype_example = id(new PhutilJSON())->encodeAsList($subtype_example);
$subtype_default = array(
array(
'key' => $subtype_default_key,
'name' => pht('Task'),
),
);
$subtype_description = $this->deformat(pht(<<<EOTEXT
Allows you to define task subtypes. Subtypes let you hide fields you don't
need to simplify the workflows for editing tasks.
To define subtypes, provide a list of subtypes. Each subtype should be a
dictionary with these keys:
- `key` //Required string.// Internal identifier for the subtype, like
"task", "feature", or "bug".
- `name` //Required string.// Human-readable name for this subtype, like
"Task", "Feature Request" or "Bug Report".
- `tag` //Optional string.// Tag text for this subtype.
- `color` //Optional string.// Display color for this subtype.
- `icon` //Optional string.// Icon for the subtype.
- `children` //Optional map.// Configure options shown to the user when
they "Create Subtask". See below.
+ - `fields` //Optional map.// Configure field behaviors. See below.
Each subtype must have a unique key, and you must define a subtype with
the key "%s", which is used as a default subtype.
The tag text (`tag`) is used to set the text shown in the subtype tag on list
views and workboards. If you do not configure it, the default subtype will have
no subtype tag and other subtypes will use their name as tag text.
The `children` key allows you to configure which options are presented to the
user when they "Create Subtask" from a task of this subtype. You can specify
these keys:
- `subtypes`: //Optional list<string>.// Show users creation forms for these
task subtypes.
- `forms`: //Optional list<string|int>.// Show users these specific forms,
in order.
If you don't specify either constraint, users will be shown creation forms
for the same subtype.
For example, if you have a "quest" subtype and do not configure `children`,
users who click "Create Subtask" will be presented with all create forms for
"quest" tasks.
If you want to present them with forms for a different task subtype or set of
subtypes instead, use `subtypes`:
```
{
...
"children": {
"subtypes": ["objective", "boss", "reward"]
}
...
}
```
If you want to present them with specific forms, use `forms` and specify form
IDs:
```
{
...
"children": {
"forms": [12, 16]
}
...
}
```
When specifying forms by ID explicitly, the order you specify the forms in will
be used when presenting options to the user.
If only one option would be presented, the user will be taken directly to the
appropriate form instead of being prompted to choose a form.
+
+The `fields` key can configure the behavior of custom fields on specific
+task subtypes. For example:
+
+```
+{
+ ...
+ "fields": {
+ "custom.some-field": {
+ "disabled": true
+ }
+ }
+ ...
+}
+```
+
+Each field supports these options:
+
+ - `disabled` //Optional bool.// Allows you to disable fields on certain
+ subtypes.
+ - `name` //Optional string.// Custom name of this field for the subtype.
+
EOTEXT
,
$subtype_default_key));
$priorities_description = $this->deformat(pht(<<<EOTEXT
Allows you to edit or override the default priorities available in Maniphest,
like "High", "Normal" and "Low". The configuration should contain a map of
numeric priority values (where larger numbers correspond to higher priorities)
to priority specifications (see defaults below for examples).
The keys you can define for a priority are:
- `name` //Required string.// Name of the priority.
- `keywords` //Required list<string>.// List of unique keywords which identify
this priority, like "high" or "low". Each priority must have at least one
keyword and two priorities may not share the same keyword.
- `short` //Optional string.// Alternate shorter name, used in UIs where
there is less space available.
- `color` //Optional string.// Color for this priority, like "red" or
"blue".
- `disabled` //Optional bool.// Set to true to prevent users from choosing
this priority when creating or editing tasks. Existing tasks will not be
affected, and can be batch edited to a different priority or left to
eventually die out.
You can choose the default priority for newly created tasks with
"maniphest.default-priority".
EOTEXT
));
return array(
$this->newOption('maniphest.custom-field-definitions', 'wild', array())
->setSummary(pht('Custom Maniphest fields.'))
->setDescription(
pht(
'Array of custom fields for Maniphest tasks. For details on '.
'adding custom fields to Maniphest, see "Configuring Custom '.
'Fields" in the documentation.'))
->addExample($fields_json, pht('Valid setting')),
$this->newOption('maniphest.fields', $custom_field_type, $default_fields)
->setCustomData(id(new ManiphestTask())->getCustomFieldBaseClass())
->setDescription(pht('Select and reorder task fields.')),
$this->newOption(
'maniphest.priorities',
$priority_type,
$priority_defaults)
->setSummary(pht('Configure Maniphest priority names.'))
->setDescription($priorities_description),
$this->newOption('maniphest.statuses', $status_type, $status_defaults)
->setSummary(pht('Configure Maniphest task statuses.'))
->setDescription($status_description)
->addExample($status_example, pht('Minimal Valid Config')),
$this->newOption('maniphest.default-priority', 'int', 90)
->setSummary(pht('Default task priority for create flows.'))
->setDescription(
pht(
'Choose a default priority for newly created tasks. You can '.
'review and adjust available priorities by using the '.
'%s configuration option. The default value (`90`) '.
'corresponds to the default "Needs Triage" priority.',
'maniphest.priorities')),
$this->newOption('maniphest.points', $points_type, array())
->setSummary(pht('Configure point values for tasks.'))
->setDescription($points_description)
->addExample($points_json_1, pht('Points Config'))
->addExample($points_json_2, pht('Hours Config')),
$this->newOption('maniphest.subtypes', $subtype_type, $subtype_default)
->setSummary(pht('Define task subtypes.'))
->setDescription($subtype_description)
->addExample($subtype_example, pht('Simple Subtypes')),
);
}
}
diff --git a/src/applications/maniphest/constants/ManiphestTaskStatus.php b/src/applications/maniphest/constants/ManiphestTaskStatus.php
index 4d58816e2..c040befee 100644
--- a/src/applications/maniphest/constants/ManiphestTaskStatus.php
+++ b/src/applications/maniphest/constants/ManiphestTaskStatus.php
@@ -1,368 +1,400 @@
<?php
/**
* @task validate Configuration Validation
*/
final class ManiphestTaskStatus extends ManiphestConstants {
const STATUS_OPEN = 'open';
const STATUS_CLOSED_RESOLVED = 'resolved';
const STATUS_CLOSED_WONTFIX = 'wontfix';
const STATUS_CLOSED_INVALID = 'invalid';
const STATUS_CLOSED_DUPLICATE = 'duplicate';
const STATUS_CLOSED_SPITE = 'spite';
const SPECIAL_DEFAULT = 'default';
const SPECIAL_CLOSED = 'closed';
const SPECIAL_DUPLICATE = 'duplicate';
+ const LOCKED_COMMENTS = 'comments';
+ const LOCKED_EDITS = 'edits';
+
private static function getStatusConfig() {
return PhabricatorEnv::getEnvConfig('maniphest.statuses');
}
private static function getEnabledStatusMap() {
$spec = self::getStatusConfig();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
foreach ($spec as $const => $status) {
if ($is_serious && !empty($status['silly'])) {
unset($spec[$const]);
continue;
}
}
return $spec;
}
public static function getTaskStatusMap() {
return ipull(self::getEnabledStatusMap(), 'name');
}
/**
* Get the statuses and their command keywords.
*
* @return map Statuses to lists of command keywords.
*/
public static function getTaskStatusKeywordsMap() {
$map = self::getEnabledStatusMap();
foreach ($map as $key => $spec) {
$words = idx($spec, 'keywords', array());
if (!is_array($words)) {
$words = array($words);
}
// For statuses, we include the status name because it's usually
// at least somewhat meaningful.
$words[] = $key;
foreach ($words as $word_key => $word) {
$words[$word_key] = phutil_utf8_strtolower($word);
}
$words = array_unique($words);
$map[$key] = $words;
}
return $map;
}
public static function getTaskStatusName($status) {
return self::getStatusAttribute($status, 'name', pht('Unknown Status'));
}
public static function getTaskStatusFullName($status) {
$name = self::getStatusAttribute($status, 'name.full');
if ($name !== null) {
return $name;
}
return self::getStatusAttribute($status, 'name', pht('Unknown Status'));
}
public static function renderFullDescription($status, $priority) {
if (self::isOpenStatus($status)) {
$name = pht('%s, %s', self::getTaskStatusFullName($status), $priority);
$color = 'grey';
$icon = 'fa-square-o';
} else {
$name = self::getTaskStatusFullName($status);
$color = 'indigo';
$icon = 'fa-check-square-o';
}
$tag = id(new PHUITagView())
->setName($name)
->setIcon($icon)
->setType(PHUITagView::TYPE_SHADE)
->setColor($color);
return $tag;
}
private static function getSpecialStatus($special) {
foreach (self::getStatusConfig() as $const => $status) {
if (idx($status, 'special') == $special) {
return $const;
}
}
return null;
}
public static function getDefaultStatus() {
return self::getSpecialStatus(self::SPECIAL_DEFAULT);
}
public static function getDefaultClosedStatus() {
return self::getSpecialStatus(self::SPECIAL_CLOSED);
}
public static function getDuplicateStatus() {
return self::getSpecialStatus(self::SPECIAL_DUPLICATE);
}
public static function getOpenStatusConstants() {
$result = array();
foreach (self::getEnabledStatusMap() as $const => $status) {
if (empty($status['closed'])) {
$result[] = $const;
}
}
return $result;
}
public static function getClosedStatusConstants() {
$all = array_keys(self::getTaskStatusMap());
$open = self::getOpenStatusConstants();
return array_diff($all, $open);
}
public static function isOpenStatus($status) {
foreach (self::getOpenStatusConstants() as $constant) {
if ($status == $constant) {
return true;
}
}
return false;
}
public static function isClaimStatus($status) {
return self::getStatusAttribute($status, 'claim', true);
}
public static function isClosedStatus($status) {
return !self::isOpenStatus($status);
}
- public static function isLockedStatus($status) {
- return self::getStatusAttribute($status, 'locked', false);
+ public static function areCommentsLockedInStatus($status) {
+ return (bool)self::getStatusAttribute($status, 'locked', false);
+ }
+
+ public static function areEditsLockedInStatus($status) {
+ $locked = self::getStatusAttribute($status, 'locked');
+ return ($locked === self::LOCKED_EDITS);
}
public static function isMFAStatus($status) {
return self::getStatusAttribute($status, 'mfa', false);
}
public static function getStatusActionName($status) {
return self::getStatusAttribute($status, 'name.action');
}
public static function getStatusColor($status) {
return self::getStatusAttribute($status, 'transaction.color');
}
public static function isDisabledStatus($status) {
return self::getStatusAttribute($status, 'disabled');
}
public static function getStatusIcon($status) {
$icon = self::getStatusAttribute($status, 'transaction.icon');
if ($icon) {
return $icon;
}
if (self::isOpenStatus($status)) {
return 'fa-exclamation-circle';
} else {
return 'fa-check-square-o';
}
}
public static function getStatusPrefixMap() {
$map = array();
foreach (self::getEnabledStatusMap() as $const => $status) {
foreach (idx($status, 'prefixes', array()) as $prefix) {
$map[$prefix] = $const;
}
}
$map += array(
'ref' => null,
'refs' => null,
'references' => null,
'cf.' => null,
);
return $map;
}
public static function getStatusSuffixMap() {
$map = array();
foreach (self::getEnabledStatusMap() as $const => $status) {
foreach (idx($status, 'suffixes', array()) as $prefix) {
$map[$prefix] = $const;
}
}
return $map;
}
private static function getStatusAttribute($status, $key, $default = null) {
$config = self::getStatusConfig();
$spec = idx($config, $status);
if ($spec) {
return idx($spec, $key, $default);
}
return $default;
}
/* -( Configuration Validation )------------------------------------------- */
/**
* @task validate
*/
public static function isValidStatusConstant($constant) {
if (!strlen($constant) || strlen($constant) > 64) {
return false;
}
// Alphanumeric, but not exclusively numeric
if (!preg_match('/^(?![0-9]*$)[a-zA-Z0-9]+$/', $constant)) {
return false;
}
return true;
}
/**
* @task validate
*/
public static function validateConfiguration(array $config) {
foreach ($config as $key => $value) {
if (!self::isValidStatusConstant($key)) {
throw new Exception(
pht(
'Key "%s" is not a valid status constant. Status constants '.
'must be 1-64 alphanumeric characters and cannot be exclusively '.
'digits. For example, "%s" or "%s" are reasonable choices.',
$key,
'open',
'closed'));
}
if (!is_array($value)) {
throw new Exception(
pht(
'Value for key "%s" should be a dictionary.',
$key));
}
PhutilTypeSpec::checkMap(
$value,
array(
'name' => 'string',
'name.full' => 'optional string',
'name.action' => 'optional string',
'closed' => 'optional bool',
'special' => 'optional string',
'transaction.icon' => 'optional string',
'transaction.color' => 'optional string',
'silly' => 'optional bool',
'prefixes' => 'optional list<string>',
'suffixes' => 'optional list<string>',
'keywords' => 'optional list<string>',
'disabled' => 'optional bool',
'claim' => 'optional bool',
- 'locked' => 'optional bool',
+ 'locked' => 'optional bool|string',
'mfa' => 'optional bool',
));
}
+ // Supported values are "comments" or "edits". For backward compatibility,
+ // "true" is an alias of "comments".
+
+ foreach ($config as $key => $value) {
+ $locked = idx($value, 'locked', false);
+ if ($locked === true || $locked === false) {
+ continue;
+ }
+
+ if ($locked === self::LOCKED_EDITS ||
+ $locked === self::LOCKED_COMMENTS) {
+ continue;
+ }
+
+ throw new Exception(
+ pht(
+ 'Task status ("%s") has unrecognized value for "locked" '.
+ 'configuration ("%s"). Supported values are: "%s", "%s".',
+ $key,
+ $locked,
+ self::LOCKED_COMMENTS,
+ self::LOCKED_EDITS));
+ }
+
$special_map = array();
foreach ($config as $key => $value) {
$special = idx($value, 'special');
if (!$special) {
continue;
}
if (isset($special_map[$special])) {
throw new Exception(
pht(
'Configuration has two statuses both marked with the special '.
'attribute "%s" ("%s" and "%s"). There should be only one.',
$special,
$special_map[$special],
$key));
}
switch ($special) {
case self::SPECIAL_DEFAULT:
if (!empty($value['closed'])) {
throw new Exception(
pht(
'Status "%s" is marked as default, but it is a closed '.
'status. The default status should be an open status.',
$key));
}
break;
case self::SPECIAL_CLOSED:
if (empty($value['closed'])) {
throw new Exception(
pht(
'Status "%s" is marked as the default status for closing '.
'tasks, but is not a closed status. It should be a closed '.
'status.',
$key));
}
break;
case self::SPECIAL_DUPLICATE:
if (empty($value['closed'])) {
throw new Exception(
pht(
'Status "%s" is marked as the status for closing tasks as '.
'duplicates, but it is not a closed status. It should '.
'be a closed status.',
$key));
}
break;
}
$special_map[$special] = $key;
}
// NOTE: We're not explicitly validating that we have at least one open
// and one closed status, because the DEFAULT and CLOSED specials imply
// that to be true. If those change in the future, that might become a
// reasonable thing to validate.
$required = array(
self::SPECIAL_DEFAULT,
self::SPECIAL_CLOSED,
self::SPECIAL_DUPLICATE,
);
foreach ($required as $required_special) {
if (!isset($special_map[$required_special])) {
throw new Exception(
pht(
'Configuration defines no task status with special attribute '.
'"%s", but you must specify a status which fills this special '.
'role.',
$required_special));
}
}
}
}
diff --git a/src/applications/maniphest/controller/ManiphestController.php b/src/applications/maniphest/controller/ManiphestController.php
index c80a1a462..970095009 100644
--- a/src/applications/maniphest/controller/ManiphestController.php
+++ b/src/applications/maniphest/controller/ManiphestController.php
@@ -1,64 +1,138 @@
<?php
abstract class ManiphestController extends PhabricatorController {
public function buildApplicationMenu() {
return $this->buildSideNavView()->getMenu();
}
public function buildSideNavView() {
$viewer = $this->getViewer();
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
id(new ManiphestTaskSearchEngine())
->setViewer($viewer)
->addNavigationItems($nav->getMenu());
if ($viewer->isLoggedIn()) {
// For now, don't give logged-out users access to reports.
$nav->addLabel(pht('Reports'));
$nav->addFilter('report', pht('Reports'));
}
$nav->selectFilter(null);
return $nav;
}
protected function buildApplicationCrumbs() {
$crumbs = parent::buildApplicationCrumbs();
id(new ManiphestEditEngine())
->setViewer($this->getViewer())
->addActionToCrumbs($crumbs);
return $crumbs;
}
- public function renderSingleTask(ManiphestTask $task) {
- $request = $this->getRequest();
- $user = $request->getUser();
+ final protected function newTaskGraphDropdownMenu(
+ ManiphestTask $task,
+ $has_parents,
+ $has_subtasks,
+ $include_standalone) {
+ $viewer = $this->getViewer();
+
+ $parents_uri = urisprintf(
+ '/?subtaskIDs=%d#R',
+ $task->getID());
+ $parents_uri = $this->getApplicationURI($parents_uri);
+
+ $subtasks_uri = urisprintf(
+ '/?parentIDs=%d#R',
+ $task->getID());
+ $subtasks_uri = $this->getApplicationURI($subtasks_uri);
- $phids = $task->getProjectPHIDs();
- if ($task->getOwnerPHID()) {
- $phids[] = $task->getOwnerPHID();
+ $dropdown_menu = id(new PhabricatorActionListView())
+ ->setViewer($viewer)
+ ->addAction(
+ id(new PhabricatorActionView())
+ ->setHref($parents_uri)
+ ->setName(pht('Search Parent Tasks'))
+ ->setDisabled(!$has_parents)
+ ->setIcon('fa-chevron-circle-up'))
+ ->addAction(
+ id(new PhabricatorActionView())
+ ->setHref($subtasks_uri)
+ ->setName(pht('Search Subtasks'))
+ ->setDisabled(!$has_subtasks)
+ ->setIcon('fa-chevron-circle-down'));
+
+ if ($include_standalone) {
+ $standalone_uri = urisprintf('/graph/%d/', $task->getID());
+ $standalone_uri = $this->getApplicationURI($standalone_uri);
+
+ $dropdown_menu->addAction(
+ id(new PhabricatorActionView())
+ ->setHref($standalone_uri)
+ ->setName(pht('View Standalone Graph'))
+ ->setIcon('fa-code-fork'));
}
- $handles = id(new PhabricatorHandleQuery())
- ->setViewer($user)
- ->withPHIDs($phids)
- ->execute();
+ $graph_menu = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setIcon('fa-search')
+ ->setText(pht('Search...'))
+ ->setDropdownMenu($dropdown_menu);
+
+ return $graph_menu;
+ }
- $view = id(new ManiphestTaskListView())
- ->setUser($user)
- ->setShowSubpriorityControls(!$request->getStr('ungrippable'))
- ->setShowBatchControls(true)
- ->setHandles($handles)
- ->setTasks(array($task));
+ final protected function newTaskGraphOverflowView(
+ ManiphestTask $task,
+ $overflow_message,
+ $include_standalone) {
- return $view;
+ $id = $task->getID();
+
+ if ($include_standalone) {
+ $standalone_uri = $this->getApplicationURI("graph/{$id}/");
+
+ $standalone_link = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setHref($standalone_uri)
+ ->setColor(PHUIButtonView::GREY)
+ ->setIcon('fa-code-fork')
+ ->setText(pht('View Standalone Graph'));
+ } else {
+ $standalone_link = null;
+ }
+
+ $standalone_icon = id(new PHUIIconView())
+ ->setIcon('fa-exclamation-triangle', 'yellow')
+ ->addClass('object-graph-header-icon');
+
+ $standalone_view = phutil_tag(
+ 'div',
+ array(
+ 'class' => 'object-graph-header',
+ ),
+ array(
+ $standalone_link,
+ $standalone_icon,
+ phutil_tag(
+ 'div',
+ array(
+ 'class' => 'object-graph-header-message',
+ ),
+ array(
+ $overflow_message,
+ )),
+ ));
+
+ return $standalone_view;
}
+
}
diff --git a/src/applications/maniphest/controller/ManiphestReportController.php b/src/applications/maniphest/controller/ManiphestReportController.php
index 77bd6c0d5..40498e6e4 100644
--- a/src/applications/maniphest/controller/ManiphestReportController.php
+++ b/src/applications/maniphest/controller/ManiphestReportController.php
@@ -1,873 +1,882 @@
<?php
final class ManiphestReportController extends ManiphestController {
private $view;
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$this->view = $request->getURIData('view');
if ($request->isFormPost()) {
$uri = $request->getRequestURI();
$project = head($request->getArr('set_project'));
$project = nonempty($project, null);
- $uri = $uri->alter('project', $project);
+
+ if ($project !== null) {
+ $uri->replaceQueryParam('project', $project);
+ } else {
+ $uri->removeQueryParam('project');
+ }
$window = $request->getStr('set_window');
- $uri = $uri->alter('window', $window);
+ if ($window !== null) {
+ $uri->replaceQueryParam('window', $window);
+ } else {
+ $uri->removeQueryParam('window');
+ }
return id(new AphrontRedirectResponse())->setURI($uri);
}
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI('/maniphest/report/'));
$nav->addLabel(pht('Open Tasks'));
$nav->addFilter('user', pht('By User'));
$nav->addFilter('project', pht('By Project'));
$nav->addLabel(pht('Burnup'));
$nav->addFilter('burn', pht('Burnup Rate'));
$this->view = $nav->selectFilter($this->view, 'user');
require_celerity_resource('maniphest-report-css');
switch ($this->view) {
case 'burn':
$core = $this->renderBurn();
break;
case 'user':
case 'project':
$core = $this->renderOpenTasks();
break;
default:
return new Aphront404Response();
}
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb(pht('Reports'));
$nav->appendChild($core);
$title = pht('Maniphest Reports');
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->setNavigation($nav);
}
public function renderBurn() {
$request = $this->getRequest();
$viewer = $request->getUser();
$handle = null;
$project_phid = $request->getStr('project');
if ($project_phid) {
$phids = array($project_phid);
$handles = $this->loadViewerHandles($phids);
$handle = $handles[$project_phid];
}
$table = new ManiphestTransaction();
$conn = $table->establishConnection('r');
if ($project_phid) {
$joins = qsprintf(
$conn,
'JOIN %T t ON x.objectPHID = t.phid
JOIN %T p ON p.src = t.phid AND p.type = %d AND p.dst = %s',
id(new ManiphestTask())->getTableName(),
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$project_phid);
$create_joins = qsprintf(
$conn,
'JOIN %T p ON p.src = t.phid AND p.type = %d AND p.dst = %s',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$project_phid);
} else {
$joins = qsprintf($conn, '');
$create_joins = qsprintf($conn, '');
}
$data = queryfx_all(
$conn,
'SELECT x.transactionType, x.oldValue, x.newValue, x.dateCreated
FROM %T x %Q
WHERE transactionType IN (%Ls)
ORDER BY x.dateCreated ASC',
$table->getTableName(),
$joins,
array(
ManiphestTaskStatusTransaction::TRANSACTIONTYPE,
ManiphestTaskMergedIntoTransaction::TRANSACTIONTYPE,
));
// See PHI273. After the move to EditEngine, we no longer create a
// "status" transaction if a task is created directly into the default
// status. This likely impacted API/email tasks after 2016 and all other
// tasks after late 2017. Until Facts can fix this properly, use the
// task creation dates to generate synthetic transactions which look like
// the older transactions that this page expects.
$default_status = ManiphestTaskStatus::getDefaultStatus();
$duplicate_status = ManiphestTaskStatus::getDuplicateStatus();
// Build synthetic transactions which take status from `null` to the
// default value.
$create_rows = queryfx_all(
$conn,
'SELECT t.dateCreated FROM %T t %Q',
id(new ManiphestTask())->getTableName(),
$create_joins);
foreach ($create_rows as $key => $create_row) {
$create_rows[$key] = array(
'transactionType' => 'status',
'oldValue' => null,
'newValue' => $default_status,
'dateCreated' => $create_row['dateCreated'],
);
}
// Remove any actual legacy status transactions which take status from
// `null` to any open status.
foreach ($data as $key => $row) {
if ($row['transactionType'] != 'status') {
continue;
}
$oldv = trim($row['oldValue'], '"');
$newv = trim($row['newValue'], '"');
// If this is a status change, preserve it.
if ($oldv != 'null') {
continue;
}
// If this task was created directly into a closed status, preserve
// the transaction.
if (!ManiphestTaskStatus::isOpenStatus($newv)) {
continue;
}
// If this is a legacy "create" transaction, discard it in favor of the
// synthetic one.
unset($data[$key]);
}
// Merge the synthetic rows into the real transactions.
$data = array_merge($create_rows, $data);
$data = array_values($data);
$data = isort($data, 'dateCreated');
$stats = array();
$day_buckets = array();
$open_tasks = array();
foreach ($data as $key => $row) {
switch ($row['transactionType']) {
case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
// NOTE: Hack to avoid json_decode().
$oldv = trim($row['oldValue'], '"');
$newv = trim($row['newValue'], '"');
break;
case ManiphestTaskMergedIntoTransaction::TRANSACTIONTYPE:
// NOTE: Merging a task does not generate a "status" transaction.
// We pretend it did. Note that this is not always accurate: it is
// possible to merge a task which was previously closed, but this
// fake transaction always counts a merge as a closure.
$oldv = $default_status;
$newv = $duplicate_status;
break;
}
if ($oldv == 'null') {
$old_is_open = false;
} else {
$old_is_open = ManiphestTaskStatus::isOpenStatus($oldv);
}
$new_is_open = ManiphestTaskStatus::isOpenStatus($newv);
$is_open = ($new_is_open && !$old_is_open);
$is_close = ($old_is_open && !$new_is_open);
$data[$key]['_is_open'] = $is_open;
$data[$key]['_is_close'] = $is_close;
if (!$is_open && !$is_close) {
// This is either some kind of bogus event, or a resolution change
// (e.g., resolved -> invalid). Just skip it.
continue;
}
$day_bucket = phabricator_format_local_time(
$row['dateCreated'],
$viewer,
'Yz');
$day_buckets[$day_bucket] = $row['dateCreated'];
if (empty($stats[$day_bucket])) {
$stats[$day_bucket] = array(
'open' => 0,
'close' => 0,
);
}
$stats[$day_bucket][$is_close ? 'close' : 'open']++;
}
$template = array(
'open' => 0,
'close' => 0,
);
$rows = array();
$rowc = array();
$last_month = null;
$last_month_epoch = null;
$last_week = null;
$last_week_epoch = null;
$week = null;
$month = null;
$last = last_key($stats) - 1;
$period = $template;
foreach ($stats as $bucket => $info) {
$epoch = $day_buckets[$bucket];
$week_bucket = phabricator_format_local_time(
$epoch,
$viewer,
'YW');
if ($week_bucket != $last_week) {
if ($week) {
$rows[] = $this->formatBurnRow(
pht('Week of %s', phabricator_date($last_week_epoch, $viewer)),
$week);
$rowc[] = 'week';
}
$week = $template;
$last_week = $week_bucket;
$last_week_epoch = $epoch;
}
$month_bucket = phabricator_format_local_time(
$epoch,
$viewer,
'Ym');
if ($month_bucket != $last_month) {
if ($month) {
$rows[] = $this->formatBurnRow(
phabricator_format_local_time($last_month_epoch, $viewer, 'F, Y'),
$month);
$rowc[] = 'month';
}
$month = $template;
$last_month = $month_bucket;
$last_month_epoch = $epoch;
}
$rows[] = $this->formatBurnRow(phabricator_date($epoch, $viewer), $info);
$rowc[] = null;
$week['open'] += $info['open'];
$week['close'] += $info['close'];
$month['open'] += $info['open'];
$month['close'] += $info['close'];
$period['open'] += $info['open'];
$period['close'] += $info['close'];
}
if ($week) {
$rows[] = $this->formatBurnRow(
pht('Week To Date'),
$week);
$rowc[] = 'week';
}
if ($month) {
$rows[] = $this->formatBurnRow(
pht('Month To Date'),
$month);
$rowc[] = 'month';
}
$rows[] = $this->formatBurnRow(
pht('All Time'),
$period);
$rowc[] = 'aggregate';
$rows = array_reverse($rows);
$rowc = array_reverse($rowc);
$table = new AphrontTableView($rows);
$table->setRowClasses($rowc);
$table->setHeaders(
array(
pht('Period'),
pht('Opened'),
pht('Closed'),
pht('Change'),
));
$table->setColumnClasses(
array(
'right wide',
'n',
'n',
'n',
));
if ($handle) {
$inst = pht(
'NOTE: This table reflects tasks currently in '.
'the project. If a task was opened in the past but added to '.
'the project recently, it is counted on the day it was '.
'opened, not the day it was categorized. If a task was part '.
'of this project in the past but no longer is, it is not '.
'counted at all.');
$header = pht('Task Burn Rate for Project %s', $handle->renderLink());
$caption = phutil_tag('p', array(), $inst);
} else {
$header = pht('Task Burn Rate for All Tasks');
$caption = null;
}
if ($caption) {
$caption = id(new PHUIInfoView())
->appendChild($caption)
->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
}
$panel = new PHUIObjectBoxView();
$panel->setHeaderText($header);
if ($caption) {
$panel->setInfoView($caption);
}
$panel->setTable($table);
$tokens = array();
if ($handle) {
$tokens = array($handle);
}
$filter = $this->renderReportFilters($tokens, $has_window = false);
$id = celerity_generate_unique_node_id();
$chart = phutil_tag(
'div',
array(
'id' => $id,
'style' => 'border: 1px solid #BFCFDA; '.
'background-color: #fff; '.
'margin: 8px 16px; '.
'height: 400px; ',
),
'');
list($burn_x, $burn_y) = $this->buildSeries($data);
require_celerity_resource('d3');
require_celerity_resource('phui-chart-css');
Javelin::initBehavior('line-chart', array(
'hardpoint' => $id,
'x' => array(
$burn_x,
),
'y' => array(
$burn_y,
),
'xformat' => 'epoch',
'yformat' => 'int',
));
$box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Burnup Rate'))
->appendChild($chart);
return array($filter, $box, $panel);
}
private function renderReportFilters(array $tokens, $has_window) {
$request = $this->getRequest();
$viewer = $request->getUser();
$form = id(new AphrontFormView())
->setUser($viewer)
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorProjectDatasource())
->setLabel(pht('Project'))
->setLimit(1)
->setName('set_project')
// TODO: This is silly, but this is Maniphest reports.
->setValue(mpull($tokens, 'getPHID')));
if ($has_window) {
list($window_str, $ignored, $window_error) = $this->getWindow();
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Recently Means'))
->setName('set_window')
->setCaption(
pht('Configure the cutoff for the "Recently Closed" column.'))
->setValue($window_str)
->setError($window_error));
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Filter By Project')));
$filter = new AphrontListFilterView();
$filter->appendChild($form);
return $filter;
}
private function buildSeries(array $data) {
$out = array();
$counter = 0;
foreach ($data as $row) {
$t = (int)$row['dateCreated'];
if ($row['_is_close']) {
--$counter;
$out[$t] = $counter;
} else if ($row['_is_open']) {
++$counter;
$out[$t] = $counter;
}
}
return array(array_keys($out), array_values($out));
}
private function formatBurnRow($label, $info) {
$delta = $info['open'] - $info['close'];
$fmt = number_format($delta);
if ($delta > 0) {
$fmt = '+'.$fmt;
$fmt = phutil_tag('span', array('class' => 'red'), $fmt);
} else {
$fmt = phutil_tag('span', array('class' => 'green'), $fmt);
}
return array(
$label,
number_format($info['open']),
number_format($info['close']),
$fmt,
);
}
public function renderOpenTasks() {
$request = $this->getRequest();
$viewer = $request->getUser();
$query = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withStatuses(ManiphestTaskStatus::getOpenStatusConstants());
switch ($this->view) {
case 'project':
$query->needProjectPHIDs(true);
break;
}
$project_phid = $request->getStr('project');
$project_handle = null;
if ($project_phid) {
$phids = array($project_phid);
$handles = $this->loadViewerHandles($phids);
$project_handle = $handles[$project_phid];
$query->withEdgeLogicPHIDs(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
PhabricatorQueryConstraint::OPERATOR_OR,
$phids);
}
$tasks = $query->execute();
$recently_closed = $this->loadRecentlyClosedTasks();
$date = phabricator_date(time(), $viewer);
switch ($this->view) {
case 'user':
$result = mgroup($tasks, 'getOwnerPHID');
$leftover = idx($result, '', array());
unset($result['']);
$result_closed = mgroup($recently_closed, 'getOwnerPHID');
$leftover_closed = idx($result_closed, '', array());
unset($result_closed['']);
$base_link = '/maniphest/?assigned=';
$leftover_name = phutil_tag('em', array(), pht('(Up For Grabs)'));
$col_header = pht('User');
$header = pht('Open Tasks by User and Priority (%s)', $date);
break;
case 'project':
$result = array();
$leftover = array();
foreach ($tasks as $task) {
$phids = $task->getProjectPHIDs();
if ($phids) {
foreach ($phids as $project_phid) {
$result[$project_phid][] = $task;
}
} else {
$leftover[] = $task;
}
}
$result_closed = array();
$leftover_closed = array();
foreach ($recently_closed as $task) {
$phids = $task->getProjectPHIDs();
if ($phids) {
foreach ($phids as $project_phid) {
$result_closed[$project_phid][] = $task;
}
} else {
$leftover_closed[] = $task;
}
}
$base_link = '/maniphest/?projects=';
$leftover_name = phutil_tag('em', array(), pht('(No Project)'));
$col_header = pht('Project');
$header = pht('Open Tasks by Project and Priority (%s)', $date);
break;
}
$phids = array_keys($result);
$handles = $this->loadViewerHandles($phids);
$handles = msort($handles, 'getName');
$order = $request->getStr('order', 'name');
list($order, $reverse) = AphrontTableView::parseSort($order);
require_celerity_resource('aphront-tooltip-css');
Javelin::initBehavior('phabricator-tooltips', array());
$rows = array();
$pri_total = array();
foreach (array_merge($handles, array(null)) as $handle) {
if ($handle) {
if (($project_handle) &&
($project_handle->getPHID() == $handle->getPHID())) {
// If filtering by, e.g., "bugs", don't show a "bugs" group.
continue;
}
$tasks = idx($result, $handle->getPHID(), array());
$name = phutil_tag(
'a',
array(
'href' => $base_link.$handle->getPHID(),
),
$handle->getName());
$closed = idx($result_closed, $handle->getPHID(), array());
} else {
$tasks = $leftover;
$name = $leftover_name;
$closed = $leftover_closed;
}
$taskv = $tasks;
$tasks = mgroup($tasks, 'getPriority');
$row = array();
$row[] = $name;
$total = 0;
foreach (ManiphestTaskPriority::getTaskPriorityMap() as $pri => $label) {
$n = count(idx($tasks, $pri, array()));
if ($n == 0) {
$row[] = '-';
} else {
$row[] = number_format($n);
}
$total += $n;
}
$row[] = number_format($total);
list($link, $oldest_all) = $this->renderOldest($taskv);
$row[] = $link;
$normal_or_better = array();
foreach ($taskv as $id => $task) {
// TODO: This is sort of a hard-code for the default "normal" status.
// When reports are more powerful, this should be made more general.
if ($task->getPriority() < 50) {
continue;
}
$normal_or_better[$id] = $task;
}
list($link, $oldest_pri) = $this->renderOldest($normal_or_better);
$row[] = $link;
if ($closed) {
$task_ids = implode(',', mpull($closed, 'getID'));
$row[] = phutil_tag(
'a',
array(
'href' => '/maniphest/?ids='.$task_ids,
'target' => '_blank',
),
number_format(count($closed)));
} else {
$row[] = '-';
}
switch ($order) {
case 'total':
$row['sort'] = $total;
break;
case 'oldest-all':
$row['sort'] = $oldest_all;
break;
case 'oldest-pri':
$row['sort'] = $oldest_pri;
break;
case 'closed':
$row['sort'] = count($closed);
break;
case 'name':
default:
$row['sort'] = $handle ? $handle->getName() : '~';
break;
}
$rows[] = $row;
}
$rows = isort($rows, 'sort');
foreach ($rows as $k => $row) {
unset($rows[$k]['sort']);
}
if ($reverse) {
$rows = array_reverse($rows);
}
$cname = array($col_header);
$cclass = array('pri right wide');
$pri_map = ManiphestTaskPriority::getShortNameMap();
foreach ($pri_map as $pri => $label) {
$cname[] = $label;
$cclass[] = 'n';
}
$cname[] = pht('Total');
$cclass[] = 'n';
$cname[] = javelin_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => pht('Oldest open task.'),
'size' => 200,
),
),
pht('Oldest (All)'));
$cclass[] = 'n';
$cname[] = javelin_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => pht(
'Oldest open task, excluding those with Low or Wishlist priority.'),
'size' => 200,
),
),
pht('Oldest (Pri)'));
$cclass[] = 'n';
list($ignored, $window_epoch) = $this->getWindow();
$edate = phabricator_datetime($window_epoch, $viewer);
$cname[] = javelin_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => pht('Closed after %s', $edate),
'size' => 260,
),
),
pht('Recently Closed'));
$cclass[] = 'n';
$table = new AphrontTableView($rows);
$table->setHeaders($cname);
$table->setColumnClasses($cclass);
$table->makeSortable(
$request->getRequestURI(),
'order',
$order,
$reverse,
array(
'name',
null,
null,
null,
null,
null,
null,
'total',
'oldest-all',
'oldest-pri',
'closed',
));
$panel = new PHUIObjectBoxView();
$panel->setHeaderText($header);
$panel->setTable($table);
$tokens = array();
if ($project_handle) {
$tokens = array($project_handle);
}
$filter = $this->renderReportFilters($tokens, $has_window = true);
return array($filter, $panel);
}
/**
* Load all the tasks that have been recently closed.
*/
private function loadRecentlyClosedTasks() {
list($ignored, $window_epoch) = $this->getWindow();
$table = new ManiphestTask();
$xtable = new ManiphestTransaction();
$conn_r = $table->establishConnection('r');
// TODO: Gross. This table is not meant to be queried like this. Build
// real stats tables.
$open_status_list = array();
foreach (ManiphestTaskStatus::getOpenStatusConstants() as $constant) {
$open_status_list[] = json_encode((string)$constant);
}
$rows = queryfx_all(
$conn_r,
'SELECT t.id FROM %T t JOIN %T x ON x.objectPHID = t.phid
WHERE t.status NOT IN (%Ls)
AND x.oldValue IN (null, %Ls)
AND x.newValue NOT IN (%Ls)
AND t.dateModified >= %d
AND x.dateCreated >= %d',
$table->getTableName(),
$xtable->getTableName(),
ManiphestTaskStatus::getOpenStatusConstants(),
$open_status_list,
$open_status_list,
$window_epoch,
$window_epoch);
if (!$rows) {
return array();
}
$ids = ipull($rows, 'id');
$query = id(new ManiphestTaskQuery())
->setViewer($this->getRequest()->getUser())
->withIDs($ids);
switch ($this->view) {
case 'project':
$query->needProjectPHIDs(true);
break;
}
return $query->execute();
}
/**
* Parse the "Recently Means" filter into:
*
* - A string representation, like "12 AM 7 days ago" (default);
* - a locale-aware epoch representation; and
* - a possible error.
*/
private function getWindow() {
$request = $this->getRequest();
$viewer = $request->getUser();
$window_str = $this->getRequest()->getStr('window', '12 AM 7 days ago');
$error = null;
$window_epoch = null;
// Do locale-aware parsing so that the user's timezone is assumed for
// time windows like "3 PM", rather than assuming the server timezone.
$window_epoch = PhabricatorTime::parseLocalTime($window_str, $viewer);
if (!$window_epoch) {
$error = 'Invalid';
$window_epoch = time() - (60 * 60 * 24 * 7);
}
// If the time ends up in the future, convert it to the corresponding time
// and equal distance in the past. This is so users can type "6 days" (which
// means "6 days from now") and get the behavior of "6 days ago", rather
// than no results (because the window epoch is in the future). This might
// be a little confusing because it causes "tomorrow" to mean "yesterday"
// and "2022" (or whatever) to mean "ten years ago", but these inputs are
// nonsense anyway.
if ($window_epoch > time()) {
$window_epoch = time() - ($window_epoch - time());
}
return array($window_str, $window_epoch, $error);
}
private function renderOldest(array $tasks) {
assert_instances_of($tasks, 'ManiphestTask');
$oldest = null;
foreach ($tasks as $id => $task) {
if (($oldest === null) ||
($task->getDateCreated() < $tasks[$oldest]->getDateCreated())) {
$oldest = $id;
}
}
if ($oldest === null) {
return array('-', 0);
}
$oldest = $tasks[$oldest];
$raw_age = (time() - $oldest->getDateCreated());
$age = number_format($raw_age / (24 * 60 * 60)).' d';
$link = javelin_tag(
'a',
array(
'href' => '/T'.$oldest->getID(),
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => 'T'.$oldest->getID().': '.$oldest->getTitle(),
),
'target' => '_blank',
),
$age);
return array($link, $raw_age);
}
}
diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php
index 0f96d76b9..c5dba7d3b 100644
--- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php
+++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php
@@ -1,605 +1,600 @@
<?php
final class ManiphestTaskDetailController extends ManiphestController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$task = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withIDs(array($id))
->needSubscriberPHIDs(true)
->executeOne();
if (!$task) {
return new Aphront404Response();
}
$field_list = PhabricatorCustomField::getObjectFields(
$task,
PhabricatorCustomField::ROLE_VIEW);
$field_list
->setViewer($viewer)
->readFieldsFromStorage($task);
$edit_engine = id(new ManiphestEditEngine())
->setViewer($viewer)
->setTargetObject($task);
$edge_types = array(
ManiphestTaskHasCommitEdgeType::EDGECONST,
ManiphestTaskHasRevisionEdgeType::EDGECONST,
ManiphestTaskHasMockEdgeType::EDGECONST,
PhabricatorObjectMentionedByObjectEdgeType::EDGECONST,
PhabricatorObjectMentionsObjectEdgeType::EDGECONST,
ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST,
);
$phid = $task->getPHID();
$query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($phid))
->withEdgeTypes($edge_types);
$edges = idx($query->execute(), $phid);
$phids = array_fill_keys($query->getDestinationPHIDs(), true);
if ($task->getOwnerPHID()) {
$phids[$task->getOwnerPHID()] = true;
}
$phids[$task->getAuthorPHID()] = true;
$phids = array_keys($phids);
$handles = $viewer->loadHandles($phids);
$timeline = $this->buildTransactionTimeline(
$task,
new ManiphestTransactionQuery());
$monogram = $task->getMonogram();
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb($monogram)
->setBorder(true);
$header = $this->buildHeaderView($task);
$details = $this->buildPropertyView($task, $field_list, $edges, $handles);
$description = $this->buildDescriptionView($task);
$curtain = $this->buildCurtain($task, $edit_engine);
$title = pht('%s %s', $monogram, $task->getTitle());
$comment_view = $edit_engine
->buildEditEngineCommentView($task);
$timeline->setQuoteRef($monogram);
$comment_view->setTransactionTimeline($timeline);
$related_tabs = array();
$graph_menu = null;
- $graph_limit = 100;
+ $graph_limit = 200;
+ $overflow_message = null;
$task_graph = id(new ManiphestTaskGraph())
->setViewer($viewer)
->setSeedPHID($task->getPHID())
->setLimit($graph_limit)
->loadGraph();
if (!$task_graph->isEmpty()) {
$parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST;
$subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST;
$parent_map = $task_graph->getEdges($parent_type);
$subtask_map = $task_graph->getEdges($subtask_type);
$parent_list = idx($parent_map, $task->getPHID(), array());
$subtask_list = idx($subtask_map, $task->getPHID(), array());
$has_parents = (bool)$parent_list;
$has_subtasks = (bool)$subtask_list;
- $search_text = pht('Search...');
-
// First, get a count of direct parent tasks and subtasks. If there
// are too many of these, we just don't draw anything. You can use
// the search button to browse tasks with the search UI instead.
$direct_count = count($parent_list) + count($subtask_list);
if ($direct_count > $graph_limit) {
- $message = pht(
- 'Task graph too large to display (this task is directly connected '.
- 'to more than %s other tasks). Use %s to explore connected tasks.',
- $graph_limit,
- phutil_tag('strong', array(), $search_text));
- $message = phutil_tag('em', array(), $message);
- $graph_table = id(new PHUIPropertyListView())
- ->addTextContent($message);
+ $overflow_message = pht(
+ 'This task is directly connected to more than %s other tasks. '.
+ 'Use %s to browse parents or subtasks, or %s to show more of the '.
+ 'graph.',
+ new PhutilNumber($graph_limit),
+ phutil_tag('strong', array(), pht('Search...')),
+ phutil_tag('strong', array(), pht('View Standalone Graph')));
+
+ $graph_table = null;
} else {
// If there aren't too many direct tasks, but there are too many total
// tasks, we'll only render directly connected tasks.
if ($task_graph->isOverLimit()) {
$task_graph->setRenderOnlyAdjacentNodes(true);
+
+ $overflow_message = pht(
+ 'This task is connected to more than %s other tasks. '.
+ 'Only direct parents and subtasks are shown here. Use '.
+ '%s to show more of the graph.',
+ new PhutilNumber($graph_limit),
+ phutil_tag('strong', array(), pht('View Standalone Graph')));
}
+
$graph_table = $task_graph->newGraphTable();
}
- $parents_uri = urisprintf(
- '/?subtaskIDs=%d#R',
- $task->getID());
- $parents_uri = $this->getApplicationURI($parents_uri);
-
- $subtasks_uri = urisprintf(
- '/?parentIDs=%d#R',
- $task->getID());
- $subtasks_uri = $this->getApplicationURI($subtasks_uri);
-
- $dropdown_menu = id(new PhabricatorActionListView())
- ->setViewer($viewer)
- ->addAction(
- id(new PhabricatorActionView())
- ->setHref($parents_uri)
- ->setName(pht('Search Parent Tasks'))
- ->setDisabled(!$has_parents)
- ->setIcon('fa-chevron-circle-up'))
- ->addAction(
- id(new PhabricatorActionView())
- ->setHref($subtasks_uri)
- ->setName(pht('Search Subtasks'))
- ->setDisabled(!$has_subtasks)
- ->setIcon('fa-chevron-circle-down'));
-
- $graph_menu = id(new PHUIButtonView())
- ->setTag('a')
- ->setIcon('fa-search')
- ->setText($search_text)
- ->setDropdownMenu($dropdown_menu);
+ if ($overflow_message) {
+ $overflow_view = $this->newTaskGraphOverflowView(
+ $task,
+ $overflow_message,
+ true);
+
+ $graph_table = array(
+ $overflow_view,
+ $graph_table,
+ );
+ }
+
+ $graph_menu = $this->newTaskGraphDropdownMenu(
+ $task,
+ $has_parents,
+ $has_subtasks,
+ true);
$related_tabs[] = id(new PHUITabView())
->setName(pht('Task Graph'))
->setKey('graph')
->appendChild($graph_table);
}
$related_tabs[] = $this->newMocksTab($task, $query);
$related_tabs[] = $this->newMentionsTab($task, $query);
$related_tabs[] = $this->newDuplicatesTab($task, $query);
$tab_view = null;
$related_tabs = array_filter($related_tabs);
if ($related_tabs) {
$tab_group = new PHUITabGroupView();
foreach ($related_tabs as $tab) {
$tab_group->addTab($tab);
}
$related_header = id(new PHUIHeaderView())
->setHeader(pht('Related Objects'));
if ($graph_menu) {
$related_header->addActionLink($graph_menu);
}
$tab_view = id(new PHUIObjectBoxView())
->setHeader($related_header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addTabGroup($tab_group);
}
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(
array(
$tab_view,
$timeline,
$comment_view,
))
->addPropertySection(pht('Description'), $description)
->addPropertySection(pht('Details'), $details);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->setPageObjectPHIDs(
array(
$task->getPHID(),
))
->appendChild($view);
}
private function buildHeaderView(ManiphestTask $task) {
$view = id(new PHUIHeaderView())
->setHeader($task->getTitle())
->setUser($this->getRequest()->getUser())
->setPolicyObject($task);
$priority_name = ManiphestTaskPriority::getTaskPriorityName(
$task->getPriority());
$priority_color = ManiphestTaskPriority::getTaskPriorityColor(
$task->getPriority());
$status = $task->getStatus();
$status_name = ManiphestTaskStatus::renderFullDescription(
$status, $priority_name);
$view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_name);
$view->setHeaderIcon(ManiphestTaskStatus::getStatusIcon(
$task->getStatus()).' '.$priority_color);
if (ManiphestTaskPoints::getIsEnabled()) {
$points = $task->getPoints();
if ($points !== null) {
$points_name = pht('%s %s',
$task->getPoints(),
ManiphestTaskPoints::getPointsLabel());
$tag = id(new PHUITagView())
->setName($points_name)
->setColor(PHUITagView::COLOR_BLUE)
->setType(PHUITagView::TYPE_SHADE);
$view->addTag($tag);
}
}
$subtype = $task->newSubtypeObject();
if ($subtype && $subtype->hasTagView()) {
$subtype_tag = $subtype->newTagView();
$view->addTag($subtype_tag);
}
return $view;
}
private function buildCurtain(
ManiphestTask $task,
PhabricatorEditEngine $edit_engine) {
$viewer = $this->getViewer();
$id = $task->getID();
$phid = $task->getPHID();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$task,
PhabricatorPolicyCapability::CAN_EDIT);
$can_interact = PhabricatorPolicyFilter::canInteract($viewer, $task);
// We expect a policy dialog if you can't edit the task, and expect a
// lock override dialog if you can't interact with it.
$workflow_edit = (!$can_edit || !$can_interact);
$curtain = $this->newCurtainView($task);
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Task'))
->setIcon('fa-pencil')
->setHref($this->getApplicationURI("/task/edit/{$id}/"))
->setDisabled(!$can_edit)
->setWorkflow($workflow_edit));
$subtype_map = $task->newEditEngineSubtypeMap();
$subtask_options = $subtype_map->getCreateFormsForSubtype(
$edit_engine,
$task);
// If no forms are available, we want to show the user an error.
// If one form is available, we take them user directly to the form.
// If two or more forms are available, we give the user a choice.
// The "subtask" controller handles the first case (no forms) and the
// third case (more than one form). In the case of one form, we link
// directly to the form.
$subtask_uri = "/task/subtask/{$id}/";
$subtask_workflow = true;
if (count($subtask_options) == 1) {
$subtask_form = head($subtask_options);
$form_key = $subtask_form->getIdentifier();
$subtask_uri = id(new PhutilURI("/task/edit/form/{$form_key}/"))
- ->setQueryParam('parent', $id)
- ->setQueryParam('template', $id)
- ->setQueryParam('status', ManiphestTaskStatus::getDefaultStatus());
+ ->replaceQueryParam('parent', $id)
+ ->replaceQueryParam('template', $id)
+ ->replaceQueryParam('status', ManiphestTaskStatus::getDefaultStatus());
$subtask_workflow = false;
}
$subtask_uri = $this->getApplicationURI($subtask_uri);
$subtask_item = id(new PhabricatorActionView())
->setName(pht('Create Subtask'))
->setHref($subtask_uri)
->setIcon('fa-level-down')
->setDisabled(!$subtask_options)
->setWorkflow($subtask_workflow);
$relationship_list = PhabricatorObjectRelationshipList::newForObject(
$viewer,
$task);
$submenu_actions = array(
$subtask_item,
ManiphestTaskHasParentRelationship::RELATIONSHIPKEY,
ManiphestTaskHasSubtaskRelationship::RELATIONSHIPKEY,
ManiphestTaskMergeInRelationship::RELATIONSHIPKEY,
ManiphestTaskCloseAsDuplicateRelationship::RELATIONSHIPKEY,
);
$task_submenu = $relationship_list->newActionSubmenu($submenu_actions)
->setName(pht('Edit Related Tasks...'))
->setIcon('fa-anchor');
$curtain->addAction($task_submenu);
$relationship_submenu = $relationship_list->newActionMenu();
if ($relationship_submenu) {
$curtain->addAction($relationship_submenu);
}
$owner_phid = $task->getOwnerPHID();
$author_phid = $task->getAuthorPHID();
$handles = $viewer->loadHandles(array($owner_phid, $author_phid));
if ($owner_phid) {
$image_uri = $handles[$owner_phid]->getImageURI();
$image_href = $handles[$owner_phid]->getURI();
$owner = $viewer->renderHandle($owner_phid)->render();
$content = phutil_tag('strong', array(), $owner);
$assigned_to = id(new PHUIHeadThingView())
->setImage($image_uri)
->setImageHref($image_href)
->setContent($content);
} else {
$assigned_to = phutil_tag('em', array(), pht('None'));
}
$curtain->newPanel()
->setHeaderText(pht('Assigned To'))
->appendChild($assigned_to);
$author_uri = $handles[$author_phid]->getImageURI();
$author_href = $handles[$author_phid]->getURI();
$author = $viewer->renderHandle($author_phid)->render();
$content = phutil_tag('strong', array(), $author);
$date = phabricator_date($task->getDateCreated(), $viewer);
$content = pht('%s, %s', $content, $date);
$authored_by = id(new PHUIHeadThingView())
->setImage($author_uri)
->setImageHref($author_href)
->setContent($content);
$curtain->newPanel()
->setHeaderText(pht('Authored By'))
->appendChild($authored_by);
return $curtain;
}
private function buildPropertyView(
ManiphestTask $task,
PhabricatorCustomFieldList $field_list,
array $edges,
$handles) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer);
$source = $task->getOriginalEmailSource();
if ($source) {
$subject = '[T'.$task->getID().'] '.$task->getTitle();
$view->addProperty(
pht('From Email'),
phutil_tag(
'a',
array(
'href' => 'mailto:'.$source.'?subject='.$subject,
),
$source));
}
$edge_types = array(
ManiphestTaskHasRevisionEdgeType::EDGECONST
=> pht('Differential Revisions'),
);
$revisions_commits = array();
$commit_phids = array_keys(
$edges[ManiphestTaskHasCommitEdgeType::EDGECONST]);
if ($commit_phids) {
$commit_drev = DiffusionCommitHasRevisionEdgeType::EDGECONST;
$drev_edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($commit_phids)
->withEdgeTypes(array($commit_drev))
->execute();
foreach ($commit_phids as $phid) {
$revisions_commits[$phid] = $handles->renderHandle($phid)
->setShowHovercard(true)
->setShowStateIcon(true);
$revision_phid = key($drev_edges[$phid][$commit_drev]);
$revision_handle = $handles->getHandleIfExists($revision_phid);
if ($revision_handle) {
$task_drev = ManiphestTaskHasRevisionEdgeType::EDGECONST;
unset($edges[$task_drev][$revision_phid]);
$revisions_commits[$phid] = hsprintf(
'%s / %s',
$revision_handle->renderHovercardLink($revision_handle->getName()),
$revisions_commits[$phid]);
}
}
}
foreach ($edge_types as $edge_type => $edge_name) {
if (!$edges[$edge_type]) {
continue;
}
$edge_handles = $viewer->loadHandles(array_keys($edges[$edge_type]));
$edge_list = $edge_handles->renderList()
->setShowStateIcons(true);
$view->addProperty($edge_name, $edge_list);
}
if ($revisions_commits) {
$view->addProperty(
pht('Commits'),
phutil_implode_html(phutil_tag('br'), $revisions_commits));
}
$field_list->appendFieldsToPropertyList(
$task,
$viewer,
$view);
if ($view->hasAnyProperties()) {
return $view;
}
return null;
}
private function buildDescriptionView(ManiphestTask $task) {
$viewer = $this->getViewer();
$section = null;
$description = $task->getDescription();
if (strlen($description)) {
$section = new PHUIPropertyListView();
$section->addTextContent(
phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
id(new PHUIRemarkupView($viewer, $description))
->setContextObject($task)));
}
return $section;
}
private function newMocksTab(
ManiphestTask $task,
PhabricatorEdgeQuery $edge_query) {
$mock_type = ManiphestTaskHasMockEdgeType::EDGECONST;
$mock_phids = $edge_query->getDestinationPHIDs(array(), array($mock_type));
if (!$mock_phids) {
return null;
}
$viewer = $this->getViewer();
$handles = $viewer->loadHandles($mock_phids);
// TODO: It would be nice to render this as pinboard-style thumbnails,
// similar to "{M123}", instead of a list of links.
$view = id(new PHUIPropertyListView())
->addProperty(pht('Mocks'), $handles->renderList());
return id(new PHUITabView())
->setName(pht('Mocks'))
->setKey('mocks')
->appendChild($view);
}
private function newMentionsTab(
ManiphestTask $task,
PhabricatorEdgeQuery $edge_query) {
$in_type = PhabricatorObjectMentionedByObjectEdgeType::EDGECONST;
$out_type = PhabricatorObjectMentionsObjectEdgeType::EDGECONST;
$in_phids = $edge_query->getDestinationPHIDs(array(), array($in_type));
$out_phids = $edge_query->getDestinationPHIDs(array(), array($out_type));
// Filter out any mentioned users from the list. These are not generally
// very interesting to show in a relationship summary since they usually
// end up as subscribers anyway.
$user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
foreach ($out_phids as $key => $out_phid) {
if (phid_get_type($out_phid) == $user_type) {
unset($out_phids[$key]);
}
}
if (!$in_phids && !$out_phids) {
return null;
}
$viewer = $this->getViewer();
$in_handles = $viewer->loadHandles($in_phids);
$out_handles = $viewer->loadHandles($out_phids);
$in_handles = $this->getCompleteHandles($in_handles);
$out_handles = $this->getCompleteHandles($out_handles);
if (!count($in_handles) && !count($out_handles)) {
return null;
}
$view = new PHUIPropertyListView();
if (count($in_handles)) {
$view->addProperty(pht('Mentioned In'), $in_handles->renderList());
}
if (count($out_handles)) {
$view->addProperty(pht('Mentioned Here'), $out_handles->renderList());
}
return id(new PHUITabView())
->setName(pht('Mentions'))
->setKey('mentions')
->appendChild($view);
}
private function newDuplicatesTab(
ManiphestTask $task,
PhabricatorEdgeQuery $edge_query) {
$in_type = ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST;
$in_phids = $edge_query->getDestinationPHIDs(array(), array($in_type));
$viewer = $this->getViewer();
$in_handles = $viewer->loadHandles($in_phids);
$in_handles = $this->getCompleteHandles($in_handles);
$view = new PHUIPropertyListView();
if (!count($in_handles)) {
return null;
}
$view->addProperty(
pht('Duplicates Merged Here'), $in_handles->renderList());
return id(new PHUITabView())
->setName(pht('Duplicates'))
->setKey('duplicates')
->appendChild($view);
}
private function getCompleteHandles(PhabricatorHandleList $handles) {
$phids = array();
foreach ($handles as $phid => $handle) {
if (!$handle->isComplete()) {
continue;
}
$phids[] = $phid;
}
return $handles->newSublist($phids);
}
}
diff --git a/src/applications/maniphest/controller/ManiphestTaskEditController.php b/src/applications/maniphest/controller/ManiphestTaskEditController.php
index 948352913..341997e32 100644
--- a/src/applications/maniphest/controller/ManiphestTaskEditController.php
+++ b/src/applications/maniphest/controller/ManiphestTaskEditController.php
@@ -1,16 +1,15 @@
<?php
final class ManiphestTaskEditController extends ManiphestController {
public function handleRequest(AphrontRequest $request) {
return id(new ManiphestEditEngine())
->setController($this)
- ->addContextParameter('ungrippable')
->addContextParameter('responseType')
->addContextParameter('columnPHID')
->addContextParameter('order')
->addContextParameter('visiblePHIDs')
->buildResponse();
}
}
diff --git a/src/applications/maniphest/controller/ManiphestTaskGraphController.php b/src/applications/maniphest/controller/ManiphestTaskGraphController.php
new file mode 100644
index 000000000..f4655d183
--- /dev/null
+++ b/src/applications/maniphest/controller/ManiphestTaskGraphController.php
@@ -0,0 +1,126 @@
+<?php
+
+final class ManiphestTaskGraphController
+ extends ManiphestController {
+
+ public function shouldAllowPublic() {
+ return true;
+ }
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+ $id = $request->getURIData('id');
+
+ $task = id(new ManiphestTaskQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($id))
+ ->executeOne();
+ if (!$task) {
+ return new Aphront404Response();
+ }
+
+ $crumbs = $this->buildApplicationCrumbs()
+ ->addTextCrumb($task->getMonogram(), $task->getURI())
+ ->addTextCrumb(pht('Graph'))
+ ->setBorder(true);
+
+ $graph_limit = 2000;
+ $overflow_message = null;
+ $task_graph = id(new ManiphestTaskGraph())
+ ->setViewer($viewer)
+ ->setSeedPHID($task->getPHID())
+ ->setLimit($graph_limit)
+ ->setIsStandalone(true)
+ ->loadGraph();
+ if (!$task_graph->isEmpty()) {
+ $parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST;
+ $subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST;
+ $parent_map = $task_graph->getEdges($parent_type);
+ $subtask_map = $task_graph->getEdges($subtask_type);
+ $parent_list = idx($parent_map, $task->getPHID(), array());
+ $subtask_list = idx($subtask_map, $task->getPHID(), array());
+ $has_parents = (bool)$parent_list;
+ $has_subtasks = (bool)$subtask_list;
+
+ // First, get a count of direct parent tasks and subtasks. If there
+ // are too many of these, we just don't draw anything. You can use
+ // the search button to browse tasks with the search UI instead.
+ $direct_count = count($parent_list) + count($subtask_list);
+
+ if ($direct_count > $graph_limit) {
+ $overflow_message = pht(
+ 'This task is directly connected to more than %s other tasks, '.
+ 'which is too many tasks to display. Use %s to browse parents '.
+ 'or subtasks.',
+ new PhutilNumber($graph_limit),
+ phutil_tag('strong', array(), pht('Search...')));
+
+ $graph_table = null;
+ } else {
+ // If there aren't too many direct tasks, but there are too many total
+ // tasks, we'll only render directly connected tasks.
+ if ($task_graph->isOverLimit()) {
+ $task_graph->setRenderOnlyAdjacentNodes(true);
+
+ $overflow_message = pht(
+ 'This task is connected to more than %s other tasks. '.
+ 'Only direct parents and subtasks are shown here.',
+ new PhutilNumber($graph_limit));
+ }
+
+ $graph_table = $task_graph->newGraphTable();
+ }
+
+ $graph_menu = $this->newTaskGraphDropdownMenu(
+ $task,
+ $has_parents,
+ $has_subtasks,
+ false);
+ } else {
+ $graph_menu = null;
+ $graph_table = null;
+
+ $overflow_message = pht(
+ 'This task has no parent tasks and no subtasks, so there is no '.
+ 'graph to draw.');
+ }
+
+ if ($overflow_message) {
+ $overflow_view = $this->newTaskGraphOverflowView(
+ $task,
+ $overflow_message,
+ false);
+
+ $graph_table = array(
+ $overflow_view,
+ $graph_table,
+ );
+ }
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader(pht('Task Graph'));
+
+ if ($graph_menu) {
+ $header->addActionLink($graph_menu);
+ }
+
+ $tab_view = id(new PHUIObjectBoxView())
+ ->setHeader($header)
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->appendChild($graph_table);
+
+ $view = id(new PHUITwoColumnView())
+ ->setFooter($tab_view);
+
+ return $this->newPage()
+ ->setTitle(
+ array(
+ $task->getMonogram(),
+ pht('Graph'),
+ ))
+ ->setCrumbs($crumbs)
+ ->appendChild($view);
+ }
+
+
+}
diff --git a/src/applications/maniphest/controller/ManiphestTaskSubtaskController.php b/src/applications/maniphest/controller/ManiphestTaskSubtaskController.php
index 5256c1bd2..3105cf661 100644
--- a/src/applications/maniphest/controller/ManiphestTaskSubtaskController.php
+++ b/src/applications/maniphest/controller/ManiphestTaskSubtaskController.php
@@ -1,71 +1,71 @@
<?php
final class ManiphestTaskSubtaskController
extends ManiphestController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$task = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$task) {
return new Aphront404Response();
}
$cancel_uri = $task->getURI();
$edit_engine = id(new ManiphestEditEngine())
->setViewer($viewer)
->setTargetObject($task);
$subtype_map = $task->newEditEngineSubtypeMap();
$subtype_options = $subtype_map->getCreateFormsForSubtype(
$edit_engine,
$task);
if (!$subtype_options) {
return $this->newDialog()
->setTitle(pht('No Forms'))
->appendParagraph(
pht(
'You do not have access to any forms which can be used to '.
'create a subtask.'))
->addCancelButton($cancel_uri, pht('Close'));
}
$menu = id(new PHUIObjectItemListView())
->setUser($viewer)
->setBig(true)
->setFlush(true);
foreach ($subtype_options as $form_key => $subtype_form) {
$subtype_key = $subtype_form->getSubtype();
$subtype = $subtype_map->getSubtype($subtype_key);
$subtask_uri = id(new PhutilURI("/task/edit/form/{$form_key}/"))
- ->setQueryParam('parent', $id)
- ->setQueryParam('template', $id)
- ->setQueryParam('status', ManiphestTaskStatus::getDefaultStatus());
+ ->replaceQueryParam('parent', $id)
+ ->replaceQueryParam('template', $id)
+ ->replaceQueryParam('status', ManiphestTaskStatus::getDefaultStatus());
$subtask_uri = $this->getApplicationURI($subtask_uri);
$item = id(new PHUIObjectItemView())
->setHeader($subtype_form->getDisplayName())
->setHref($subtask_uri)
->setClickable(true)
->setImageIcon($subtype->newIconView())
->addAttribute($subtype->getName());
$menu->addItem($item);
}
return $this->newDialog()
->setTitle(pht('Choose Subtype'))
->appendChild($menu)
->addCancelButton($cancel_uri);
}
}
diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php
index dc9c56f84..2a8730d5c 100644
--- a/src/applications/maniphest/editor/ManiphestEditEngine.php
+++ b/src/applications/maniphest/editor/ManiphestEditEngine.php
@@ -1,529 +1,530 @@
<?php
final class ManiphestEditEngine
extends PhabricatorEditEngine {
const ENGINECONST = 'maniphest.task';
public function getEngineName() {
return pht('Maniphest Tasks');
}
public function getSummaryHeader() {
return pht('Configure Maniphest Task Forms');
}
public function getSummaryText() {
return pht('Configure how users create and edit tasks.');
}
public function getEngineApplicationClass() {
return 'PhabricatorManiphestApplication';
}
public function isDefaultQuickCreateEngine() {
return true;
}
public function getQuickCreateOrderVector() {
return id(new PhutilSortVector())->addInt(100);
}
protected function newEditableObject() {
return ManiphestTask::initializeNewTask($this->getViewer());
}
protected function newObjectQuery() {
return id(new ManiphestTaskQuery());
}
protected function getObjectCreateTitleText($object) {
return pht('Create New Task');
}
protected function getObjectEditTitleText($object) {
return pht('Edit Task: %s', $object->getTitle());
}
protected function getObjectEditShortText($object) {
return $object->getMonogram();
}
protected function getObjectCreateShortText() {
return pht('Create Task');
}
protected function getObjectName() {
return pht('Task');
}
protected function getEditorURI() {
return $this->getApplication()->getApplicationURI('task/edit/');
}
protected function getCommentViewHeaderText($object) {
return pht('Weigh In');
}
protected function getCommentViewButtonText($object) {
return pht('Set Sail for Adventure');
}
protected function getObjectViewURI($object) {
return '/'.$object->getMonogram();
}
protected function buildCustomEditFields($object) {
$status_map = $this->getTaskStatusMap($object);
$priority_map = $this->getTaskPriorityMap($object);
$alias_map = ManiphestTaskPriority::getTaskPriorityAliasMap();
if ($object->isClosed()) {
$default_status = ManiphestTaskStatus::getDefaultStatus();
} else {
$default_status = ManiphestTaskStatus::getDefaultClosedStatus();
}
if ($object->getOwnerPHID()) {
$owner_value = array($object->getOwnerPHID());
} else {
$owner_value = array($this->getViewer()->getPHID());
}
$column_documentation = pht(<<<EODOCS
You can use this transaction type to create a task into a particular workboard
column, or move an existing task between columns.
The transaction value can be specified in several forms. Some are simpler but
less powerful, while others are more complex and more powerful.
The simplest valid value is a single column PHID:
```lang=json
"PHID-PCOL-1111"
```
This will move the task into that column, or create the task into that column
if you are creating a new task. If the task is currently on the board, it will
be moved out of any exclusive columns. If the task is not currently on the
board, it will be added to the board.
You can also perform multiple moves at the same time by passing a list of
PHIDs:
```lang=json
["PHID-PCOL-2222", "PHID-PCOL-3333"]
```
This is equivalent to performing each move individually.
The most complex and most powerful form uses a dictionary to provide additional
information about the move, including an optional specific position within the
column.
The target column should be identified as `columnPHID`, and you may select a
-position by passing either `beforePHID` or `afterPHID`, specifying the PHID of
-a task currently in the column that you want to move this task before or after:
+position by passing either `beforePHIDs` or `afterPHIDs`, specifying the PHIDs
+of tasks currently in the column that you want to move this task before or
+after:
```lang=json
[
{
"columnPHID": "PHID-PCOL-4444",
- "beforePHID": "PHID-TASK-5555"
+ "beforePHIDs": ["PHID-TASK-5555"]
}
]
```
-Note that this affects only the "natural" position of the task. The task
-position when the board is sorted by some other attribute (like priority)
-depends on that attribute value: change a task's priority to move it on
-priority-sorted boards.
+When you specify multiple PHIDs, the task will be moved adjacent to the first
+valid PHID found in either of the lists. This allows positional moves to
+generally work as users expect even if the client view of the board has fallen
+out of date and some of the nearby tasks have moved elsewhere.
EODOCS
);
$column_map = $this->getColumnMap($object);
$fields = array(
id(new PhabricatorHandlesEditField())
->setKey('parent')
->setLabel(pht('Parent Task'))
->setDescription(pht('Task to make this a subtask of.'))
->setConduitDescription(pht('Create as a subtask of another task.'))
->setConduitTypeDescription(pht('PHID of the parent task.'))
->setAliases(array('parentPHID'))
->setTransactionType(ManiphestTaskParentTransaction::TRANSACTIONTYPE)
->setHandleParameterType(new ManiphestTaskListHTTPParameterType())
->setSingleValue(null)
->setIsReorderable(false)
->setIsDefaultable(false)
->setIsLockable(false),
id(new PhabricatorColumnsEditField())
->setKey('column')
->setLabel(pht('Column'))
->setDescription(pht('Create a task in a workboard column.'))
->setConduitDescription(
pht('Move a task to one or more workboard columns.'))
->setConduitTypeDescription(
pht('List of columns to move the task to.'))
->setConduitDocumentation($column_documentation)
->setAliases(array('columnPHID', 'columns', 'columnPHIDs'))
->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS)
->setIsReorderable(false)
->setIsDefaultable(false)
->setIsLockable(false)
->setCommentActionLabel(pht('Move on Workboard'))
->setCommentActionOrder(2000)
->setColumnMap($column_map),
id(new PhabricatorTextEditField())
->setKey('title')
->setLabel(pht('Title'))
->setBulkEditLabel(pht('Set title to'))
->setDescription(pht('Name of the task.'))
->setConduitDescription(pht('Rename the task.'))
->setConduitTypeDescription(pht('New task name.'))
->setTransactionType(ManiphestTaskTitleTransaction::TRANSACTIONTYPE)
->setIsRequired(true)
->setValue($object->getTitle()),
id(new PhabricatorUsersEditField())
->setKey('owner')
->setAliases(array('ownerPHID', 'assign', 'assigned'))
->setLabel(pht('Assigned To'))
->setBulkEditLabel(pht('Assign to'))
->setDescription(pht('User who is responsible for the task.'))
->setConduitDescription(pht('Reassign the task.'))
->setConduitTypeDescription(
pht('New task owner, or `null` to unassign.'))
->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE)
->setIsCopyable(true)
->setIsNullable(true)
->setSingleValue($object->getOwnerPHID())
->setCommentActionLabel(pht('Assign / Claim'))
->setCommentActionValue($owner_value),
id(new PhabricatorSelectEditField())
->setKey('status')
->setLabel(pht('Status'))
->setBulkEditLabel(pht('Set status to'))
->setDescription(pht('Status of the task.'))
->setConduitDescription(pht('Change the task status.'))
->setConduitTypeDescription(pht('New task status constant.'))
->setTransactionType(ManiphestTaskStatusTransaction::TRANSACTIONTYPE)
->setIsCopyable(true)
->setValue($object->getStatus())
->setOptions($status_map)
->setCommentActionLabel(pht('Change Status'))
->setCommentActionValue($default_status),
id(new PhabricatorSelectEditField())
->setKey('priority')
->setLabel(pht('Priority'))
->setBulkEditLabel(pht('Set priority to'))
->setDescription(pht('Priority of the task.'))
->setConduitDescription(pht('Change the priority of the task.'))
->setConduitTypeDescription(pht('New task priority constant.'))
->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE)
->setIsCopyable(true)
->setValue($object->getPriorityKeyword())
->setOptions($priority_map)
->setOptionAliases($alias_map)
->setCommentActionLabel(pht('Change Priority')),
);
if (ManiphestTaskPoints::getIsEnabled()) {
$points_label = ManiphestTaskPoints::getPointsLabel();
$action_label = ManiphestTaskPoints::getPointsActionLabel();
$fields[] = id(new PhabricatorPointsEditField())
->setKey('points')
->setLabel($points_label)
->setBulkEditLabel($action_label)
->setDescription(pht('Point value of the task.'))
->setConduitDescription(pht('Change the task point value.'))
->setConduitTypeDescription(pht('New task point value.'))
->setTransactionType(ManiphestTaskPointsTransaction::TRANSACTIONTYPE)
->setIsCopyable(true)
->setValue($object->getPoints())
->setCommentActionLabel($action_label);
}
$fields[] = id(new PhabricatorRemarkupEditField())
->setKey('description')
->setLabel(pht('Description'))
->setBulkEditLabel(pht('Set description to'))
->setDescription(pht('Task description.'))
->setConduitDescription(pht('Update the task description.'))
->setConduitTypeDescription(pht('New task description.'))
->setTransactionType(ManiphestTaskDescriptionTransaction::TRANSACTIONTYPE)
->setValue($object->getDescription())
->setPreviewPanel(
id(new PHUIRemarkupPreviewPanel())
->setHeader(pht('Description Preview')));
$parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST;
$subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST;
$src_phid = $object->getPHID();
if ($src_phid) {
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($src_phid))
->withEdgeTypes(
array(
$parent_type,
$subtask_type,
));
$edge_query->execute();
$parent_phids = $edge_query->getDestinationPHIDs(
array($src_phid),
array($parent_type));
$subtask_phids = $edge_query->getDestinationPHIDs(
array($src_phid),
array($subtask_type));
} else {
$parent_phids = array();
$subtask_phids = array();
}
$fields[] = id(new PhabricatorHandlesEditField())
->setKey('parents')
->setLabel(pht('Parents'))
->setDescription(pht('Parent tasks.'))
->setConduitDescription(pht('Change the parents of this task.'))
->setConduitTypeDescription(pht('List of parent task PHIDs.'))
->setUseEdgeTransactions(true)
->setIsFormField(false)
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $parent_type)
->setValue($parent_phids);
$fields[] = id(new PhabricatorHandlesEditField())
->setKey('subtasks')
->setLabel(pht('Subtasks'))
->setDescription(pht('Subtasks.'))
->setConduitDescription(pht('Change the subtasks of this task.'))
->setConduitTypeDescription(pht('List of subtask PHIDs.'))
->setUseEdgeTransactions(true)
->setIsFormField(false)
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $subtask_type)
->setValue($parent_phids);
return $fields;
}
private function getTaskStatusMap(ManiphestTask $task) {
$status_map = ManiphestTaskStatus::getTaskStatusMap();
$current_status = $task->getStatus();
// If the current status is something we don't recognize (maybe an older
// status which was deleted), put a dummy entry in the status map so that
// saving the form doesn't destroy any data by accident.
if (idx($status_map, $current_status) === null) {
$status_map[$current_status] = pht('<Unknown: %s>', $current_status);
}
$dup_status = ManiphestTaskStatus::getDuplicateStatus();
foreach ($status_map as $status => $status_name) {
// Always keep the task's current status.
if ($status == $current_status) {
continue;
}
// Don't allow tasks to be changed directly into "Closed, Duplicate"
// status. Instead, you have to merge them. See T4819.
if ($status == $dup_status) {
unset($status_map[$status]);
continue;
}
// Don't let new or existing tasks be moved into a disabled status.
if (ManiphestTaskStatus::isDisabledStatus($status)) {
unset($status_map[$status]);
continue;
}
}
return $status_map;
}
private function getTaskPriorityMap(ManiphestTask $task) {
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
$priority_keywords = ManiphestTaskPriority::getTaskPriorityKeywordsMap();
$current_priority = $task->getPriority();
$results = array();
foreach ($priority_map as $priority => $priority_name) {
$disabled = ManiphestTaskPriority::isDisabledPriority($priority);
if ($disabled && !($priority == $current_priority)) {
continue;
}
$keyword = head(idx($priority_keywords, $priority));
$results[$keyword] = $priority_name;
}
// If the current value isn't a legitimate one, put it in the dropdown
// anyway so saving the form doesn't cause any side effects.
if (idx($priority_map, $current_priority) === null) {
$results[ManiphestTaskPriority::UNKNOWN_PRIORITY_KEYWORD] = pht(
'<Unknown: %s>',
$current_priority);
}
return $results;
}
protected function newEditResponse(
AphrontRequest $request,
$object,
array $xactions) {
- if ($request->isAjax()) {
+ $response_type = $request->getStr('responseType');
+ $is_card = ($response_type === 'card');
+
+ if ($is_card) {
// Reload the task to make sure we pick up the final task state.
$viewer = $this->getViewer();
$task = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withIDs(array($object->getID()))
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
->executeOne();
- switch ($request->getStr('responseType')) {
- case 'card':
- return $this->buildCardResponse($task);
- default:
- return $this->buildListResponse($task);
- }
-
+ return $this->buildCardResponse($task);
}
return parent::newEditResponse($request, $object, $xactions);
}
- private function buildListResponse(ManiphestTask $task) {
- $controller = $this->getController();
-
- $payload = array(
- 'tasks' => $controller->renderSingleTask($task),
- 'data' => array(),
- );
-
- return id(new AphrontAjaxResponse())->setContent($payload);
- }
-
private function buildCardResponse(ManiphestTask $task) {
$controller = $this->getController();
$request = $controller->getRequest();
$viewer = $request->getViewer();
$column_phid = $request->getStr('columnPHID');
$visible_phids = $request->getStrList('visiblePHIDs');
if (!$visible_phids) {
$visible_phids = array();
}
$column = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withPHIDs(array($column_phid))
->executeOne();
if (!$column) {
return new Aphront404Response();
}
$board_phid = $column->getProjectPHID();
$object_phid = $task->getPHID();
- return id(new PhabricatorBoardResponseEngine())
+ $order = $request->getStr('order');
+ if ($order) {
+ $ordering = PhabricatorProjectColumnOrder::getOrderByKey($order);
+ $ordering = id(clone $ordering)
+ ->setViewer($viewer);
+ } else {
+ $ordering = null;
+ }
+
+ $engine = id(new PhabricatorBoardResponseEngine())
->setViewer($viewer)
->setBoardPHID($board_phid)
->setObjectPHID($object_phid)
- ->setVisiblePHIDs($visible_phids)
- ->buildResponse();
+ ->setVisiblePHIDs($visible_phids);
+
+ if ($ordering) {
+ $engine->setOrdering($ordering);
+ }
+
+ return $engine->buildResponse();
}
private function getColumnMap(ManiphestTask $task) {
$phid = $task->getPHID();
if (!$phid) {
return array();
}
$board_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$phid,
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
if (!$board_phids) {
return array();
}
$viewer = $this->getViewer();
$layout_engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs($board_phids)
->setObjectPHIDs(array($task->getPHID()))
->executeLayout();
$map = array();
foreach ($board_phids as $board_phid) {
$in_columns = $layout_engine->getObjectColumns($board_phid, $phid);
$in_columns = mpull($in_columns, null, 'getPHID');
$all_columns = $layout_engine->getColumns($board_phid);
if (!$all_columns) {
// This could be a project with no workboard, or a project the viewer
// does not have permission to see.
continue;
}
$board = head($all_columns)->getProject();
$options = array();
foreach ($all_columns as $column) {
$name = $column->getDisplayName();
$is_hidden = $column->isHidden();
$is_selected = isset($in_columns[$column->getPHID()]);
// Don't show hidden, subproject or milestone columns in this map
// unless the object is currently in the column.
$skip_column = ($is_hidden || $column->getProxyPHID());
if ($skip_column) {
if (!$is_selected) {
continue;
}
}
if ($is_hidden) {
$name = pht('(%s)', $name);
}
if ($is_selected) {
$name = pht("\xE2\x97\x8F %s", $name);
} else {
$name = pht("\xE2\x97\x8B %s", $name);
}
$option = array(
'key' => $column->getPHID(),
'label' => $name,
'selected' => (bool)$is_selected,
);
$options[] = $option;
}
$map[] = array(
'label' => $board->getDisplayName(),
'options' => $options,
);
}
$map = isort($map, 'label');
$map = array_values($map);
return $map;
}
}
diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
index 0722e0e27..fd5bfe0cd 100644
--- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php
+++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
@@ -1,1015 +1,857 @@
<?php
final class ManiphestTransactionEditor
extends PhabricatorApplicationTransactionEditor {
private $moreValidationErrors = array();
public function getEditorApplicationClass() {
return 'PhabricatorManiphestApplication';
}
public function getEditorObjectsDescription() {
return pht('Maniphest Tasks');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_COLUMNS;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this task.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
return null;
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
return $xaction->getNewValue();
}
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
return (bool)$new;
}
return parent::transactionHasEffect($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
return;
}
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
foreach ($xaction->getNewValue() as $move) {
$this->applyBoardMove($object, $move);
}
break;
}
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
// When we change the status of a task, update tasks this tasks blocks
// with a message to the effect of "alincoln resolved blocking task Txxx."
$unblock_xaction = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
$unblock_xaction = $xaction;
break;
}
}
if ($unblock_xaction !== null) {
$blocked_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
ManiphestTaskDependedOnByTaskEdgeType::EDGECONST);
if ($blocked_phids) {
// In theory we could apply these through policies, but that seems a
// little bit surprising. For now, use the actor's vision.
$blocked_tasks = id(new ManiphestTaskQuery())
->setViewer($this->getActor())
->withPHIDs($blocked_phids)
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
->execute();
$old = $unblock_xaction->getOldValue();
$new = $unblock_xaction->getNewValue();
foreach ($blocked_tasks as $blocked_task) {
$parent_xaction = id(new ManiphestTransaction())
->setTransactionType(
ManiphestTaskUnblockTransaction::TRANSACTIONTYPE)
->setOldValue(array($object->getPHID() => $old))
->setNewValue(array($object->getPHID() => $new));
if ($this->getIsNewObject()) {
$parent_xaction->setMetadataValue('blocker.new', true);
}
- id(new ManiphestTransactionEditor())
- ->setActor($this->getActor())
- ->setActingAsPHID($this->getActingAsPHID())
- ->setContentSource($this->getContentSource())
+ $this->newSubEditor()
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($blocked_task, array($parent_xaction));
}
}
}
return $xactions;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailSubjectPrefix() {
return pht('[Maniphest]');
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
return 'maniphest-task-'.$object->getPHID();
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$phids = array();
if ($object->getOwnerPHID()) {
$phids[] = $object->getOwnerPHID();
}
$phids[] = $this->getActingAsPHID();
return $phids;
}
public function getMailTagsMap() {
return array(
ManiphestTransaction::MAILTAG_STATUS =>
pht("A task's status changes."),
ManiphestTransaction::MAILTAG_OWNER =>
pht("A task's owner changes."),
ManiphestTransaction::MAILTAG_PRIORITY =>
pht("A task's priority changes."),
ManiphestTransaction::MAILTAG_CC =>
pht("A task's subscribers change."),
ManiphestTransaction::MAILTAG_PROJECTS =>
pht("A task's associated projects change."),
ManiphestTransaction::MAILTAG_UNBLOCK =>
pht("One of a task's subtasks changes status."),
ManiphestTransaction::MAILTAG_COLUMN =>
pht('A task is moved between columns on a workboard.'),
ManiphestTransaction::MAILTAG_COMMENT =>
pht('Someone comments on a task.'),
ManiphestTransaction::MAILTAG_OTHER =>
pht('Other task activity not listed above occurs.'),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new ManiphestReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$title = $object->getTitle();
return id(new PhabricatorMetaMTAMail())
->setSubject("T{$id}: {$title}");
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
if ($this->getIsNewObject()) {
$body->addRemarkupSection(
pht('TASK DESCRIPTION'),
$object->getDescription());
}
$body->addLinkSection(
pht('TASK DETAIL'),
PhabricatorEnv::getProductionURI('/T'.$object->getID()));
$board_phids = array();
$type_columns = PhabricatorTransactions::TYPE_COLUMNS;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $type_columns) {
$moves = $xaction->getNewValue();
foreach ($moves as $move) {
$board_phids[] = $move['boardPHID'];
}
}
}
if ($board_phids) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->requireActor())
->withPHIDs($board_phids)
->execute();
foreach ($projects as $project) {
$body->addLinkSection(
pht('WORKBOARD'),
- PhabricatorEnv::getProductionURI(
- '/project/board/'.$project->getID().'/'));
+ PhabricatorEnv::getProductionURI($project->getWorkboardURI()));
}
}
return $body;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function supportsSearch() {
return true;
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new HeraldManiphestTaskAdapter())
->setTask($object);
}
protected function adjustObjectForPolicyChecks(
PhabricatorLiskDAO $object,
array $xactions) {
$copy = parent::adjustObjectForPolicyChecks($object, $xactions);
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE:
$copy->setOwnerPHID($xaction->getNewValue());
break;
default:
break;
}
}
return $copy;
}
- /**
- * Get priorities for moving a task to a new priority.
- */
- public static function getEdgeSubpriority(
- $priority,
- $is_end) {
-
- $query = id(new ManiphestTaskQuery())
- ->setViewer(PhabricatorUser::getOmnipotentUser())
- ->withPriorities(array($priority))
- ->setLimit(1);
-
- if ($is_end) {
- $query->setOrderVector(array('-priority', '-subpriority', '-id'));
- } else {
- $query->setOrderVector(array('priority', 'subpriority', 'id'));
- }
-
- $result = $query->executeOne();
- $step = (double)(2 << 32);
-
- if ($result) {
- $base = $result->getSubpriority();
- if ($is_end) {
- $sub = ($base - $step);
- } else {
- $sub = ($base + $step);
- }
- } else {
- $sub = 0;
- }
-
- return array($priority, $sub);
- }
-
-
- /**
- * Get priorities for moving a task before or after another task.
- */
- public static function getAdjacentSubpriority(
- ManiphestTask $dst,
- $is_after) {
-
- $query = id(new ManiphestTaskQuery())
- ->setViewer(PhabricatorUser::getOmnipotentUser())
- ->setOrder(ManiphestTaskQuery::ORDER_PRIORITY)
- ->withPriorities(array($dst->getPriority()))
- ->setLimit(1);
-
- if ($is_after) {
- $query->setAfterID($dst->getID());
- } else {
- $query->setBeforeID($dst->getID());
- }
-
- $adjacent = $query->executeOne();
-
- $base = $dst->getSubpriority();
- $step = (double)(2 << 32);
-
- // If we find an adjacent task, we average the two subpriorities and
- // return the result.
- if ($adjacent) {
- $epsilon = 1.0;
-
- // If the adjacent task has a subpriority that is identical or very
- // close to the task we're looking at, we're going to spread out all
- // the nearby tasks.
-
- $adjacent_sub = $adjacent->getSubpriority();
- if ((abs($adjacent_sub - $base) < $epsilon)) {
- $base = self::disperseBlock(
- $dst,
- $epsilon * 2);
- if ($is_after) {
- $sub = $base - $epsilon;
- } else {
- $sub = $base + $epsilon;
- }
- } else {
- $sub = ($adjacent_sub + $base) / 2;
- }
- } else {
- // Otherwise, we take a step away from the target's subpriority and
- // use that.
- if ($is_after) {
- $sub = ($base - $step);
- } else {
- $sub = ($base + $step);
- }
- }
-
- return array($dst->getPriority(), $sub);
- }
-
- /**
- * Distribute a cluster of tasks with similar subpriorities.
- */
- private static function disperseBlock(
- ManiphestTask $task,
- $spacing) {
-
- $conn = $task->establishConnection('w');
-
- // Find a block of subpriority space which is, on average, sparse enough
- // to hold all the tasks that are inside it with a reasonable level of
- // separation between them.
-
- // We'll start by looking near the target task for a range of numbers
- // which has more space available than tasks. For example, if the target
- // task has subpriority 33 and we want to separate each task by at least 1,
- // we might start by looking in the range [23, 43].
-
- // If we find fewer than 20 tasks there, we have room to reassign them
- // with the desired level of separation. We space them out, then we're
- // done.
-
- // However: if we find more than 20 tasks, we don't have enough room to
- // distribute them. We'll widen our search and look in a bigger range,
- // maybe [13, 53]. This range has more space, so if we find fewer than
- // 40 tasks in this range we can spread them out. If we still find too
- // many tasks, we keep widening the search.
-
- $base = $task->getSubpriority();
-
- $scale = 4.0;
- while (true) {
- $range = ($spacing * $scale) / 2.0;
- $min = ($base - $range);
- $max = ($base + $range);
-
- $result = queryfx_one(
- $conn,
- 'SELECT COUNT(*) N FROM %T WHERE priority = %d AND
- subpriority BETWEEN %f AND %f',
- $task->getTableName(),
- $task->getPriority(),
- $min,
- $max);
-
- $count = $result['N'];
- if ($count < $scale) {
- // We have found a block which we can make sparse enough, so bail and
- // continue below with our selection.
- break;
- }
-
- // This block had too many tasks for its size, so try again with a
- // bigger block.
- $scale *= 2.0;
- }
-
- $rows = queryfx_all(
- $conn,
- 'SELECT id FROM %T WHERE priority = %d AND
- subpriority BETWEEN %f AND %f
- ORDER BY priority, subpriority, id',
- $task->getTableName(),
- $task->getPriority(),
- $min,
- $max);
-
- $task_id = $task->getID();
- $result = null;
-
- // NOTE: In strict mode (which we encourage enabling) we can't structure
- // this bulk update as an "INSERT ... ON DUPLICATE KEY UPDATE" unless we
- // provide default values for ALL of the columns that don't have defaults.
-
- // This is gross, but we may be moving enough rows that individual
- // queries are unreasonably slow. An alternate construction which might
- // be worth evaluating is to use "CASE". Another approach is to disable
- // strict mode for this query.
-
- $default_str = qsprintf($conn, '%s', '');
- $default_int = qsprintf($conn, '%d', 0);
-
- $extra_columns = array(
- 'phid' => $default_str,
- 'authorPHID' => $default_str,
- 'status' => $default_str,
- 'priority' => $default_int,
- 'title' => $default_str,
- 'description' => $default_str,
- 'dateCreated' => $default_int,
- 'dateModified' => $default_int,
- 'mailKey' => $default_str,
- 'viewPolicy' => $default_str,
- 'editPolicy' => $default_str,
- 'ownerOrdering' => $default_str,
- 'spacePHID' => $default_str,
- 'bridgedObjectPHID' => $default_str,
- 'properties' => $default_str,
- 'points' => $default_int,
- 'subtype' => $default_str,
- );
-
- $sql = array();
- $offset = 0;
-
- // Often, we'll have more room than we need in the range. Distribute the
- // tasks evenly over the whole range so that we're less likely to end up
- // with tasks spaced exactly the minimum distance apart, which may
- // get shifted again later. We have one fewer space to distribute than we
- // have tasks.
- $divisor = (double)(count($rows) - 1.0);
- if ($divisor > 0) {
- $available_distance = (($max - $min) / $divisor);
- } else {
- $available_distance = 0.0;
- }
-
- foreach ($rows as $row) {
- $subpriority = $min + ($offset * $available_distance);
-
- // If this is the task that we're spreading out relative to, keep track
- // of where it is ending up so we can return the new subpriority.
- $id = $row['id'];
- if ($id == $task_id) {
- $result = $subpriority;
- }
-
- $sql[] = qsprintf(
- $conn,
- '(%d, %LQ, %f)',
- $id,
- $extra_columns,
- $subpriority);
-
- $offset++;
- }
-
- foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
- queryfx(
- $conn,
- 'INSERT INTO %T (id, %LC, subpriority) VALUES %LQ
- ON DUPLICATE KEY UPDATE subpriority = VALUES(subpriority)',
- $task->getTableName(),
- array_keys($extra_columns),
- $chunk);
- }
-
- return $result;
- }
-
protected function validateAllTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$errors = parent::validateAllTransactions($object, $xactions);
if ($this->moreValidationErrors) {
$errors = array_merge($errors, $this->moreValidationErrors);
}
+ foreach ($this->getLockValidationErrors($object, $xactions) as $error) {
+ $errors[] = $error;
+ }
+
return $errors;
}
protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$actor = $this->getActor();
$actor_phid = $actor->getPHID();
$results = parent::expandTransactions($object, $xactions);
$is_unassigned = ($object->getOwnerPHID() === null);
$any_assign = false;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() ==
ManiphestTaskOwnerTransaction::TRANSACTIONTYPE) {
$any_assign = true;
break;
}
}
$is_open = !$object->isClosed();
$new_status = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
$new_status = $xaction->getNewValue();
break;
}
}
if ($new_status === null) {
$is_closing = false;
} else {
$is_closing = ManiphestTaskStatus::isClosedStatus($new_status);
}
// If the task is not assigned, not being assigned, currently open, and
// being closed, try to assign the actor as the owner.
if ($is_unassigned && !$any_assign && $is_open && $is_closing) {
$is_claim = ManiphestTaskStatus::isClaimStatus($new_status);
// Don't assign the actor if they aren't a real user.
// Don't claim the task if the status is configured to not claim.
if ($actor_phid && $is_claim) {
$results[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE)
->setNewValue($actor_phid);
}
}
// Automatically subscribe the author when they create a task.
if ($this->getIsNewObject()) {
if ($actor_phid) {
$results[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue(
array(
'+' => array($actor_phid => $actor_phid),
));
}
}
return $results;
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$results = parent::expandTransaction($object, $xaction);
$type = $xaction->getTransactionType();
switch ($type) {
case PhabricatorTransactions::TYPE_COLUMNS:
try {
$more_xactions = $this->buildMoveTransaction($object, $xaction);
foreach ($more_xactions as $more_xaction) {
$results[] = $more_xaction;
}
} catch (Exception $ex) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
$ex->getMessage(),
$xaction);
$this->moreValidationErrors[] = $error;
}
break;
case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE:
// If this is a no-op update, don't expand it.
$old_value = $object->getOwnerPHID();
$new_value = $xaction->getNewValue();
if ($old_value === $new_value) {
break;
}
// When a task is reassigned, move the old owner to the subscriber
// list so they're still in the loop.
if ($old_value) {
$results[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setIgnoreOnNoEffect(true)
->setNewValue(
array(
'+' => array($old_value => $old_value),
));
}
break;
}
return $results;
}
private function buildMoveTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
+ $actor = $this->getActor();
$new = $xaction->getNewValue();
if (!is_array($new)) {
$this->validateColumnPHID($new);
$new = array($new);
}
- $nearby_phids = array();
+ $relative_phids = array();
foreach ($new as $key => $value) {
if (!is_array($value)) {
$this->validateColumnPHID($value);
$value = array(
'columnPHID' => $value,
);
}
PhutilTypeSpec::checkMap(
$value,
array(
'columnPHID' => 'string',
+ 'beforePHIDs' => 'optional list<string>',
+ 'afterPHIDs' => 'optional list<string>',
+
+ // Deprecated older variations of "beforePHIDs" and "afterPHIDs".
'beforePHID' => 'optional string',
'afterPHID' => 'optional string',
));
- $new[$key] = $value;
+ $value = $value + array(
+ 'beforePHIDs' => array(),
+ 'afterPHIDs' => array(),
+ );
- if (!empty($value['beforePHID'])) {
- $nearby_phids[] = $value['beforePHID'];
+ // Normalize the legacy keys "beforePHID" and "afterPHID" keys to the
+ // modern format.
+ if (!empty($value['afterPHID'])) {
+ if ($value['afterPHIDs']) {
+ throw new Exception(
+ pht(
+ 'Transaction specifies both "afterPHID" and "afterPHIDs". '.
+ 'Specify only "afterPHIDs".'));
+ }
+ $value['afterPHIDs'] = array($value['afterPHID']);
+ unset($value['afterPHID']);
}
- if (!empty($value['afterPHID'])) {
- $nearby_phids[] = $value['afterPHID'];
+ if (isset($value['beforePHID'])) {
+ if ($value['beforePHIDs']) {
+ throw new Exception(
+ pht(
+ 'Transaction specifies both "beforePHID" and "beforePHIDs". '.
+ 'Specify only "beforePHIDs".'));
+ }
+ $value['beforePHIDs'] = array($value['beforePHID']);
+ unset($value['beforePHID']);
+ }
+
+ foreach ($value['beforePHIDs'] as $phid) {
+ $relative_phids[] = $phid;
+ }
+
+ foreach ($value['afterPHIDs'] as $phid) {
+ $relative_phids[] = $phid;
}
+
+ $new[$key] = $value;
}
- if ($nearby_phids) {
- $nearby_objects = id(new PhabricatorObjectQuery())
- ->setViewer($this->getActor())
- ->withPHIDs($nearby_phids)
+ // We require that objects you specify in "beforePHIDs" or "afterPHIDs"
+ // are real objects which exist and which you have permission to view.
+ // If you provide other objects, we remove them from the specification.
+
+ if ($relative_phids) {
+ $objects = id(new PhabricatorObjectQuery())
+ ->setViewer($actor)
+ ->withPHIDs($relative_phids)
->execute();
- $nearby_objects = mpull($nearby_objects, null, 'getPHID');
+ $objects = mpull($objects, null, 'getPHID');
} else {
- $nearby_objects = array();
+ $objects = array();
+ }
+
+ foreach ($new as $key => $value) {
+ $value['afterPHIDs'] = $this->filterValidPHIDs(
+ $value['afterPHIDs'],
+ $objects);
+ $value['beforePHIDs'] = $this->filterValidPHIDs(
+ $value['beforePHIDs'],
+ $objects);
+
+ $new[$key] = $value;
}
$column_phids = ipull($new, 'columnPHID');
if ($column_phids) {
$columns = id(new PhabricatorProjectColumnQuery())
- ->setViewer($this->getActor())
+ ->setViewer($actor)
->withPHIDs($column_phids)
->execute();
$columns = mpull($columns, null, 'getPHID');
} else {
$columns = array();
}
$board_phids = mpull($columns, 'getProjectPHID');
$object_phid = $object->getPHID();
- $object_phids = $nearby_phids;
-
// Note that we may not have an object PHID if we're creating a new
// object.
+ $object_phids = array();
if ($object_phid) {
$object_phids[] = $object_phid;
}
if ($object_phids) {
$layout_engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($this->getActor())
->setBoardPHIDs($board_phids)
->setObjectPHIDs($object_phids)
->setFetchAllBoards(true)
->executeLayout();
}
foreach ($new as $key => $spec) {
$column_phid = $spec['columnPHID'];
$column = idx($columns, $column_phid);
if (!$column) {
throw new Exception(
pht(
'Column move transaction specifies column PHID "%s", but there '.
'is no corresponding column with this PHID.',
$column_phid));
}
$board_phid = $column->getProjectPHID();
- $nearby = array();
-
- if (!empty($spec['beforePHID'])) {
- $nearby['beforePHID'] = $spec['beforePHID'];
- }
-
- if (!empty($spec['afterPHID'])) {
- $nearby['afterPHID'] = $spec['afterPHID'];
- }
-
- if (count($nearby) > 1) {
- throw new Exception(
- pht(
- 'Column move transaction moves object to multiple positions. '.
- 'Specify only "beforePHID" or "afterPHID", not both.'));
- }
-
- foreach ($nearby as $where => $nearby_phid) {
- if (empty($nearby_objects[$nearby_phid])) {
- throw new Exception(
- pht(
- 'Column move transaction specifies object "%s" as "%s", but '.
- 'there is no corresponding object with this PHID.',
- $object_phid,
- $where));
- }
-
- $nearby_columns = $layout_engine->getObjectColumns(
- $board_phid,
- $nearby_phid);
- $nearby_columns = mpull($nearby_columns, null, 'getPHID');
-
- if (empty($nearby_columns[$column_phid])) {
- throw new Exception(
- pht(
- 'Column move transaction specifies object "%s" as "%s" in '.
- 'column "%s", but this object is not in that column!',
- $nearby_phid,
- $where,
- $column_phid));
- }
- }
-
if ($object_phid) {
$old_columns = $layout_engine->getObjectColumns(
$board_phid,
$object_phid);
$old_column_phids = mpull($old_columns, 'getPHID');
} else {
$old_column_phids = array();
}
$spec += array(
'boardPHID' => $board_phid,
'fromColumnPHIDs' => $old_column_phids,
);
// Check if the object is already in this column, and isn't being moved.
// We can just drop this column change if it has no effect.
$from_map = array_fuse($spec['fromColumnPHIDs']);
$already_here = isset($from_map[$column_phid]);
- $is_reordering = (bool)$nearby;
+ $is_reordering = ($spec['afterPHIDs'] || $spec['beforePHIDs']);
if ($already_here && !$is_reordering) {
unset($new[$key]);
} else {
$new[$key] = $spec;
}
}
$new = array_values($new);
$xaction->setNewValue($new);
$more = array();
// If we're moving the object into a column and it does not already belong
// in the column, add the appropriate board. For normal columns, this
// is the board PHID. For proxy columns, it is the proxy PHID, unless the
// object is already a member of some descendant of the proxy PHID.
// The major case where this can happen is moves via the API, but it also
// happens when a user drags a task from the "Backlog" to a milestone
// column.
if ($object_phid) {
$current_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object_phid,
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
$current_phids = array_fuse($current_phids);
} else {
$current_phids = array();
}
$add_boards = array();
foreach ($new as $move) {
$column_phid = $move['columnPHID'];
$board_phid = $move['boardPHID'];
$column = $columns[$column_phid];
$proxy_phid = $column->getProxyPHID();
// If this is a normal column, add the board if the object isn't already
// associated.
if (!$proxy_phid) {
if (!isset($current_phids[$board_phid])) {
$add_boards[] = $board_phid;
}
continue;
}
// If this is a proxy column but the object is already associated with
// the proxy board, we don't need to do anything.
if (isset($current_phids[$proxy_phid])) {
continue;
}
// If this a proxy column and the object is already associated with some
// descendant of the proxy board, we also don't need to do anything.
$descendants = id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withAncestorProjectPHIDs(array($proxy_phid))
->execute();
$found_descendant = false;
foreach ($descendants as $descendant) {
if (isset($current_phids[$descendant->getPHID()])) {
$found_descendant = true;
break;
}
}
if ($found_descendant) {
continue;
}
// Otherwise, we're moving the object to a proxy column which it is not
// a member of yet, so add an association to the column's proxy board.
$add_boards[] = $proxy_phid;
}
if ($add_boards) {
$more[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
->setIgnoreOnNoEffect(true)
->setNewValue(
array(
'+' => array_fuse($add_boards),
));
}
return $more;
}
private function applyBoardMove($object, array $move) {
$board_phid = $move['boardPHID'];
$column_phid = $move['columnPHID'];
- $before_phid = idx($move, 'beforePHID');
- $after_phid = idx($move, 'afterPHID');
+
+ $before_phids = $move['beforePHIDs'];
+ $after_phids = $move['afterPHIDs'];
$object_phid = $object->getPHID();
// We're doing layout with the omnipotent viewer to make sure we don't
// remove positions in columns that exist, but which the actual actor
// can't see.
$omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
$select_phids = array($board_phid);
$descendants = id(new PhabricatorProjectQuery())
->setViewer($omnipotent_viewer)
->withAncestorProjectPHIDs($select_phids)
->execute();
foreach ($descendants as $descendant) {
$select_phids[] = $descendant->getPHID();
}
$board_tasks = id(new ManiphestTaskQuery())
->setViewer($omnipotent_viewer)
->withEdgeLogicPHIDs(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
PhabricatorQueryConstraint::OPERATOR_ANCESTOR,
array($select_phids))
->execute();
$board_tasks = mpull($board_tasks, null, 'getPHID');
$board_tasks[$object_phid] = $object;
// Make sure tasks are sorted by ID, so we lay out new positions in
// a consistent way.
$board_tasks = msort($board_tasks, 'getID');
$object_phids = array_keys($board_tasks);
$engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($omnipotent_viewer)
->setBoardPHIDs(array($board_phid))
->setObjectPHIDs($object_phids)
->executeLayout();
// TODO: This logic needs to be revised when we legitimately support
// multiple column positions.
$columns = $engine->getObjectColumns($board_phid, $object_phid);
foreach ($columns as $column) {
$engine->queueRemovePosition(
$board_phid,
$column->getPHID(),
$object_phid);
}
- if ($before_phid) {
- $engine->queueAddPositionBefore(
- $board_phid,
- $column_phid,
- $object_phid,
- $before_phid);
- } else if ($after_phid) {
- $engine->queueAddPositionAfter(
- $board_phid,
- $column_phid,
- $object_phid,
- $after_phid);
- } else {
- $engine->queueAddPosition(
- $board_phid,
- $column_phid,
- $object_phid);
- }
+ $engine->queueAddPosition(
+ $board_phid,
+ $column_phid,
+ $object_phid,
+ $after_phids,
+ $before_phids);
$engine->applyPositionUpdates();
}
private function validateColumnPHID($value) {
if (phid_get_type($value) == PhabricatorProjectColumnPHIDType::TYPECONST) {
return;
}
throw new Exception(
pht(
'When moving objects between columns on a board, columns must '.
'be identified by PHIDs. This transaction uses "%s" to identify '.
'a column, but that is not a valid column PHID.',
$value));
}
+ private function getLockValidationErrors($object, array $xactions) {
+ $errors = array();
+
+ $old_owner = $object->getOwnerPHID();
+ $old_status = $object->getStatus();
+
+ $new_owner = $old_owner;
+ $new_status = $old_status;
+
+ $owner_xaction = null;
+ $status_xaction = null;
+
+ foreach ($xactions as $xaction) {
+ switch ($xaction->getTransactionType()) {
+ case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE:
+ $new_owner = $xaction->getNewValue();
+ $owner_xaction = $xaction;
+ break;
+ case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
+ $new_status = $xaction->getNewValue();
+ $status_xaction = $xaction;
+ break;
+ }
+ }
+
+ $actor_phid = $this->getActingAsPHID();
+
+ $was_locked = ManiphestTaskStatus::areEditsLockedInStatus(
+ $old_status);
+ $now_locked = ManiphestTaskStatus::areEditsLockedInStatus(
+ $new_status);
+
+ if (!$now_locked) {
+ // If we're not ending in an edit-locked status, everything is good.
+ } else if ($new_owner !== null) {
+ // If we ending the edit with some valid owner, this is allowed for
+ // now. We might need to revisit this.
+ } else {
+ // The edits end with the task locked and unowned. No one will be able
+ // to edit it, so we forbid this. We try to be specific about what the
+ // user did wrong.
+
+ $owner_changed = ($old_owner && !$new_owner);
+ $status_changed = ($was_locked !== $now_locked);
+ $message = null;
+
+ if ($status_changed && $owner_changed) {
+ $message = pht(
+ 'You can not lock this task and unassign it at the same time '.
+ 'because no one will be able to edit it anymore. Lock the task '.
+ 'or remove the owner, but not both.');
+ $problem_xaction = $status_xaction;
+ } else if ($status_changed) {
+ $message = pht(
+ 'You can not lock this task because it does not have an owner. '.
+ 'No one would be able to edit the task. Assign the task to an '.
+ 'owner before locking it.');
+ $problem_xaction = $status_xaction;
+ } else if ($owner_changed) {
+ $message = pht(
+ 'You can not remove the owner of this task because it is locked '.
+ 'and no one would be able to edit the task. Reassign the task or '.
+ 'unlock it before removing the owner.');
+ $problem_xaction = $owner_xaction;
+ } else {
+ // If the task was already broken, we don't have a transaction to
+ // complain about so just let it through. In theory, this is
+ // impossible since policy rules should kick in before we get here.
+ }
+
+ if ($message) {
+ $errors[] = new PhabricatorApplicationTransactionValidationError(
+ $problem_xaction->getTransactionType(),
+ pht('Lock Error'),
+ $message,
+ $problem_xaction);
+ }
+ }
+
+ return $errors;
+ }
+
+ private function filterValidPHIDs($phid_list, array $object_map) {
+ foreach ($phid_list as $key => $phid) {
+ if (isset($object_map[$phid])) {
+ continue;
+ }
+
+ unset($phid_list[$key]);
+ }
+
+ return array_values($phid_list);
+ }
}
diff --git a/src/applications/maniphest/engine/ManiphestTaskUnlockEngine.php b/src/applications/maniphest/engine/ManiphestTaskUnlockEngine.php
new file mode 100644
index 000000000..b223724a7
--- /dev/null
+++ b/src/applications/maniphest/engine/ManiphestTaskUnlockEngine.php
@@ -0,0 +1,14 @@
+<?php
+
+final class ManiphestTaskUnlockEngine
+ extends PhabricatorUnlockEngine {
+
+ public function newUnlockOwnerTransactions($object, $user) {
+ return array(
+ $this->newTransaction($object)
+ ->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE)
+ ->setNewValue($user->getPHID()),
+ );
+ }
+
+}
diff --git a/src/applications/maniphest/engineextension/ManiphestHovercardEngineExtension.php b/src/applications/maniphest/engineextension/ManiphestHovercardEngineExtension.php
index 2b2ef3c93..7474f9cfa 100644
--- a/src/applications/maniphest/engineextension/ManiphestHovercardEngineExtension.php
+++ b/src/applications/maniphest/engineextension/ManiphestHovercardEngineExtension.php
@@ -1,75 +1,74 @@
<?php
final class ManiphestHovercardEngineExtension
extends PhabricatorHovercardEngineExtension {
const EXTENSIONKEY = 'maniphest';
public function isExtensionEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorManiphestApplication');
}
public function getExtensionName() {
return pht('Maniphest Tasks');
}
public function canRenderObjectHovercard($object) {
return ($object instanceof ManiphestTask);
}
public function renderHovercard(
PHUIHovercardView $hovercard,
PhabricatorObjectHandle $handle,
$task,
$data) {
$viewer = $this->getViewer();
require_celerity_resource('phui-workcard-view-css');
$id = $task->getID();
$task = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withIDs(array($id))
->needProjectPHIDs(true)
->executeOne();
$phids = array();
$owner_phid = $task->getOwnerPHID();
if ($owner_phid) {
$phids[$owner_phid] = $owner_phid;
}
foreach ($task->getProjectPHIDs() as $phid) {
$phids[$phid] = $phid;
}
$handles = $viewer->loadHandles($phids);
$handles = iterator_to_array($handles);
$card = id(new ProjectBoardTaskCard())
->setViewer($viewer)
- ->setTask($task)
- ->setCanEdit(false);
+ ->setTask($task);
$owner_phid = $task->getOwnerPHID();
if ($owner_phid) {
$owner_handle = $handles[$owner_phid];
$card->setOwner($owner_handle);
}
$project_phids = $task->getProjectPHIDs();
$project_handles = array_select_keys($handles, $project_phids);
if ($project_handles) {
$card->setProjectHandles($project_handles);
}
$item = $card->getItem();
$card = id(new PHUIObjectItemListView())
->setFlush(true)
->setItemClass('phui-workcard')
->addClass('hovercard-task-view')
->addItem($item);
$hovercard->appendChild($card);
}
}
diff --git a/src/applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php b/src/applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php
index 3fc1957b4..ef0644ddd 100644
--- a/src/applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php
+++ b/src/applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php
@@ -1,125 +1,120 @@
<?php
final class PhabricatorManiphestTaskTestDataGenerator
extends PhabricatorTestDataGenerator {
const GENERATORKEY = 'tasks';
public function getGeneratorName() {
return pht('Maniphest Tasks');
}
public function generateObject() {
$author_phid = $this->loadPhabricatorUserPHID();
$author = id(new PhabricatorUser())
->loadOneWhere('phid = %s', $author_phid);
$task = ManiphestTask::initializeNewTask($author)
- ->setSubPriority($this->generateTaskSubPriority())
->setTitle($this->generateTitle());
$content_source = $this->getLipsumContentSource();
$template = new ManiphestTransaction();
// Accumulate Transactions
$changes = array();
$changes[ManiphestTaskTitleTransaction::TRANSACTIONTYPE] =
$this->generateTitle();
$changes[ManiphestTaskDescriptionTransaction::TRANSACTIONTYPE] =
$this->generateDescription();
$changes[ManiphestTaskOwnerTransaction::TRANSACTIONTYPE] =
$this->loadOwnerPHID();
$changes[ManiphestTaskStatusTransaction::TRANSACTIONTYPE] =
$this->generateTaskStatus();
$changes[ManiphestTaskPriorityTransaction::TRANSACTIONTYPE] =
$this->generateTaskPriority();
$changes[PhabricatorTransactions::TYPE_SUBSCRIBERS] =
array('=' => $this->getCCPHIDs());
$transactions = array();
foreach ($changes as $type => $value) {
$transaction = clone $template;
$transaction->setTransactionType($type);
$transaction->setNewValue($value);
$transactions[] = $transaction;
}
$transactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
->setNewValue(
array(
'=' => array_fuse($this->getProjectPHIDs()),
));
// Apply Transactions
$editor = id(new ManiphestTransactionEditor())
->setActor($author)
->setContentSource($content_source)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($task, $transactions);
return $task;
}
public function getCCPHIDs() {
$ccs = array();
for ($i = 0; $i < rand(1, 4);$i++) {
$ccs[] = $this->loadPhabricatorUserPHID();
}
return $ccs;
}
public function getProjectPHIDs() {
$projects = array();
for ($i = 0; $i < rand(1, 4);$i++) {
$project = $this->loadOneRandom('PhabricatorProject');
if ($project) {
$projects[] = $project->getPHID();
}
}
return $projects;
}
public function loadOwnerPHID() {
if (rand(0, 3) == 0) {
return null;
} else {
return $this->loadPhabricatorUserPHID();
}
}
public function generateTitle() {
return id(new PhutilLipsumContextFreeGrammar())
->generate();
}
public function generateDescription() {
return id(new PhutilLipsumContextFreeGrammar())
->generateSeveral(rand(30, 40));
}
public function generateTaskPriority() {
$pri = array_rand(ManiphestTaskPriority::getTaskPriorityMap());
$keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap();
$keyword = head(idx($keyword_map, $pri));
return $keyword;
}
- public function generateTaskSubPriority() {
- return rand(2 << 16, 2 << 32);
- }
-
public function generateTaskStatus() {
$statuses = array_keys(ManiphestTaskStatus::getTaskStatusMap());
// Make sure 4/5th of all generated Tasks are open
$random = rand(0, 4);
if ($random != 0) {
return ManiphestTaskStatus::getDefaultStatus();
} else {
return array_rand($statuses);
}
}
}
diff --git a/src/applications/maniphest/policy/ManiphestTaskPolicyCodex.php b/src/applications/maniphest/policy/ManiphestTaskPolicyCodex.php
new file mode 100644
index 000000000..638d9bfa6
--- /dev/null
+++ b/src/applications/maniphest/policy/ManiphestTaskPolicyCodex.php
@@ -0,0 +1,70 @@
+<?php
+
+final class ManiphestTaskPolicyCodex
+ extends PhabricatorPolicyCodex {
+
+ public function getPolicyShortName() {
+ $object = $this->getObject();
+
+ if ($object->areEditsLocked()) {
+ return pht('Edits Locked');
+ }
+
+ return null;
+ }
+
+ public function getPolicyIcon() {
+ $object = $this->getObject();
+
+ if ($object->areEditsLocked()) {
+ return 'fa-lock';
+ }
+
+ return null;
+ }
+
+ public function getPolicyTagClasses() {
+ $object = $this->getObject();
+ $classes = array();
+
+ if ($object->areEditsLocked()) {
+ $classes[] = 'policy-adjusted-locked';
+ }
+
+ return $classes;
+ }
+
+ public function getPolicySpecialRuleDescriptions() {
+ $object = $this->getObject();
+
+ $rules = array();
+
+ $rules[] = $this->newRule()
+ ->setCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->setIsActive($object->areEditsLocked())
+ ->setDescription(
+ pht(
+ 'Tasks with edits locked may only be edited by their owner.'));
+
+ return $rules;
+ }
+
+ public function getPolicyForEdit($capability) {
+
+ // When a task has its edits locked, the effective edit policy is locked
+ // to "No One". However, the task owner may still bypass the lock and edit
+ // the task. When they do, we want the control in the UI to have the
+ // correct value. Return the real value stored on the object.
+
+ switch ($capability) {
+ case PhabricatorPolicyCapability::CAN_EDIT:
+ return $this->getObject()->getEditPolicy();
+ }
+
+ return parent::getPolicyForEdit($capability);
+ }
+
+}
diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php
index fc5097f4d..9e58728cf 100644
--- a/src/applications/maniphest/query/ManiphestTaskQuery.php
+++ b/src/applications/maniphest/query/ManiphestTaskQuery.php
@@ -1,995 +1,1054 @@
<?php
/**
* Query tasks by specific criteria. This class uses the higher-performance
* but less-general Maniphest indexes to satisfy queries.
*/
final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
private $taskIDs;
private $taskPHIDs;
private $authorPHIDs;
private $ownerPHIDs;
private $noOwner;
private $anyOwner;
private $subscriberPHIDs;
private $dateCreatedAfter;
private $dateCreatedBefore;
private $dateModifiedAfter;
private $dateModifiedBefore;
private $bridgedObjectPHIDs;
private $hasOpenParents;
private $hasOpenSubtasks;
private $parentTaskIDs;
private $subtaskIDs;
private $subtypes;
private $closedEpochMin;
private $closedEpochMax;
private $closerPHIDs;
private $columnPHIDs;
+ private $specificGroupByProjectPHID;
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
const STATUS_CLOSED = 'status-closed';
const STATUS_RESOLVED = 'status-resolved';
const STATUS_WONTFIX = 'status-wontfix';
const STATUS_INVALID = 'status-invalid';
const STATUS_SPITE = 'status-spite';
const STATUS_DUPLICATE = 'status-duplicate';
private $statuses;
private $priorities;
private $subpriorities;
private $groupBy = 'group-none';
const GROUP_NONE = 'group-none';
const GROUP_PRIORITY = 'group-priority';
const GROUP_OWNER = 'group-owner';
const GROUP_STATUS = 'group-status';
const GROUP_PROJECT = 'group-project';
const ORDER_PRIORITY = 'order-priority';
const ORDER_CREATED = 'order-created';
const ORDER_MODIFIED = 'order-modified';
const ORDER_TITLE = 'order-title';
private $needSubscriberPHIDs;
private $needProjectPHIDs;
public function withAuthors(array $authors) {
$this->authorPHIDs = $authors;
return $this;
}
public function withIDs(array $ids) {
$this->taskIDs = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->taskPHIDs = $phids;
return $this;
}
public function withOwners(array $owners) {
if ($owners === array()) {
throw new Exception(pht('Empty withOwners() constraint is not valid.'));
}
$no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN;
$any_owner = PhabricatorPeopleAnyOwnerDatasource::FUNCTION_TOKEN;
foreach ($owners as $k => $phid) {
if ($phid === $no_owner || $phid === null) {
$this->noOwner = true;
unset($owners[$k]);
break;
}
if ($phid === $any_owner) {
$this->anyOwner = true;
unset($owners[$k]);
break;
}
}
if ($owners) {
$this->ownerPHIDs = $owners;
}
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withPriorities(array $priorities) {
$this->priorities = $priorities;
return $this;
}
public function withSubpriorities(array $subpriorities) {
$this->subpriorities = $subpriorities;
return $this;
}
public function withSubscribers(array $subscribers) {
$this->subscriberPHIDs = $subscribers;
return $this;
}
public function setGroupBy($group) {
$this->groupBy = $group;
switch ($this->groupBy) {
case self::GROUP_NONE:
$vector = array();
break;
case self::GROUP_PRIORITY:
$vector = array('priority');
break;
case self::GROUP_OWNER:
$vector = array('owner');
break;
case self::GROUP_STATUS:
$vector = array('status');
break;
case self::GROUP_PROJECT:
$vector = array('project');
break;
}
$this->setGroupVector($vector);
return $this;
}
public function withOpenSubtasks($value) {
$this->hasOpenSubtasks = $value;
return $this;
}
public function withOpenParents($value) {
$this->hasOpenParents = $value;
return $this;
}
public function withParentTaskIDs(array $ids) {
$this->parentTaskIDs = $ids;
return $this;
}
public function withSubtaskIDs(array $ids) {
$this->subtaskIDs = $ids;
return $this;
}
public function withDateCreatedBefore($date_created_before) {
$this->dateCreatedBefore = $date_created_before;
return $this;
}
public function withDateCreatedAfter($date_created_after) {
$this->dateCreatedAfter = $date_created_after;
return $this;
}
public function withDateModifiedBefore($date_modified_before) {
$this->dateModifiedBefore = $date_modified_before;
return $this;
}
public function withDateModifiedAfter($date_modified_after) {
$this->dateModifiedAfter = $date_modified_after;
return $this;
}
public function withClosedEpochBetween($min, $max) {
$this->closedEpochMin = $min;
$this->closedEpochMax = $max;
return $this;
}
public function withCloserPHIDs(array $phids) {
$this->closerPHIDs = $phids;
return $this;
}
public function needSubscriberPHIDs($bool) {
$this->needSubscriberPHIDs = $bool;
return $this;
}
public function needProjectPHIDs($bool) {
$this->needProjectPHIDs = $bool;
return $this;
}
public function withBridgedObjectPHIDs(array $phids) {
$this->bridgedObjectPHIDs = $phids;
return $this;
}
public function withSubtypes(array $subtypes) {
$this->subtypes = $subtypes;
return $this;
}
public function withColumnPHIDs(array $column_phids) {
$this->columnPHIDs = $column_phids;
return $this;
}
+ public function withSpecificGroupByProjectPHID($project_phid) {
+ $this->specificGroupByProjectPHID = $project_phid;
+ return $this;
+ }
+
public function newResultObject() {
return new ManiphestTask();
}
protected function loadPage() {
$task_dao = new ManiphestTask();
$conn = $task_dao->establishConnection('r');
$where = $this->buildWhereClause($conn);
$group_column = qsprintf($conn, '');
switch ($this->groupBy) {
case self::GROUP_PROJECT:
$group_column = qsprintf(
$conn,
', projectGroupName.indexedObjectPHID projectGroupPHID');
break;
}
$rows = queryfx_all(
$conn,
'%Q %Q FROM %T task %Q %Q %Q %Q %Q %Q',
$this->buildSelectClause($conn),
$group_column,
$task_dao->getTableName(),
$this->buildJoinClause($conn),
$where,
$this->buildGroupClause($conn),
$this->buildHavingClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
switch ($this->groupBy) {
case self::GROUP_PROJECT:
$data = ipull($rows, null, 'id');
break;
default:
$data = $rows;
break;
}
$data = $this->didLoadRawRows($data);
$tasks = $task_dao->loadAllFromArray($data);
switch ($this->groupBy) {
case self::GROUP_PROJECT:
$results = array();
foreach ($rows as $row) {
$task = clone $tasks[$row['id']];
$task->attachGroupByProjectPHID($row['projectGroupPHID']);
$results[] = $task;
}
$tasks = $results;
break;
}
return $tasks;
}
protected function willFilterPage(array $tasks) {
if ($this->groupBy == self::GROUP_PROJECT) {
// We should only return project groups which the user can actually see.
$project_phids = mpull($tasks, 'getGroupByProjectPHID');
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withPHIDs($project_phids)
->execute();
$projects = mpull($projects, null, 'getPHID');
foreach ($tasks as $key => $task) {
if (!$task->getGroupByProjectPHID()) {
// This task is either not tagged with any projects, or only tagged
// with projects which we're ignoring because they're being queried
// for explicitly.
continue;
}
if (empty($projects[$task->getGroupByProjectPHID()])) {
unset($tasks[$key]);
}
}
}
return $tasks;
}
protected function didFilterPage(array $tasks) {
$phids = mpull($tasks, 'getPHID');
if ($this->needProjectPHIDs) {
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($phids)
->withEdgeTypes(
array(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
));
$edge_query->execute();
foreach ($tasks as $task) {
$project_phids = $edge_query->getDestinationPHIDs(
array($task->getPHID()));
$task->attachProjectPHIDs($project_phids);
}
}
if ($this->needSubscriberPHIDs) {
$subscriber_sets = id(new PhabricatorSubscribersQuery())
->withObjectPHIDs($phids)
->execute();
foreach ($tasks as $task) {
$subscribers = idx($subscriber_sets, $task->getPHID(), array());
$task->attachSubscriberPHIDs($subscribers);
}
}
return $tasks;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
$where[] = $this->buildStatusWhereClause($conn);
$where[] = $this->buildOwnerWhereClause($conn);
if ($this->taskIDs !== null) {
$where[] = qsprintf(
$conn,
'task.id in (%Ld)',
$this->taskIDs);
}
if ($this->taskPHIDs !== null) {
$where[] = qsprintf(
$conn,
'task.phid in (%Ls)',
$this->taskPHIDs);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'task.status IN (%Ls)',
$this->statuses);
}
if ($this->authorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'task.authorPHID in (%Ls)',
$this->authorPHIDs);
}
if ($this->dateCreatedAfter) {
$where[] = qsprintf(
$conn,
'task.dateCreated >= %d',
$this->dateCreatedAfter);
}
if ($this->dateCreatedBefore) {
$where[] = qsprintf(
$conn,
'task.dateCreated <= %d',
$this->dateCreatedBefore);
}
if ($this->dateModifiedAfter) {
$where[] = qsprintf(
$conn,
'task.dateModified >= %d',
$this->dateModifiedAfter);
}
if ($this->dateModifiedBefore) {
$where[] = qsprintf(
$conn,
'task.dateModified <= %d',
$this->dateModifiedBefore);
}
if ($this->closedEpochMin !== null) {
$where[] = qsprintf(
$conn,
'task.closedEpoch >= %d',
$this->closedEpochMin);
}
if ($this->closedEpochMax !== null) {
$where[] = qsprintf(
$conn,
'task.closedEpoch <= %d',
$this->closedEpochMax);
}
if ($this->closerPHIDs !== null) {
$where[] = qsprintf(
$conn,
'task.closerPHID IN (%Ls)',
$this->closerPHIDs);
}
if ($this->priorities !== null) {
$where[] = qsprintf(
$conn,
'task.priority IN (%Ld)',
$this->priorities);
}
- if ($this->subpriorities !== null) {
- $where[] = qsprintf(
- $conn,
- 'task.subpriority IN (%Lf)',
- $this->subpriorities);
- }
-
if ($this->bridgedObjectPHIDs !== null) {
$where[] = qsprintf(
$conn,
'task.bridgedObjectPHID IN (%Ls)',
$this->bridgedObjectPHIDs);
}
if ($this->subtypes !== null) {
$where[] = qsprintf(
$conn,
'task.subtype IN (%Ls)',
$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);
}
+ if ($this->specificGroupByProjectPHID !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'projectGroupName.indexedObjectPHID = %s',
+ $this->specificGroupByProjectPHID);
+ }
+
return $where;
}
private function buildStatusWhereClause(AphrontDatabaseConnection $conn) {
static $map = array(
self::STATUS_RESOLVED => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
self::STATUS_WONTFIX => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
self::STATUS_INVALID => ManiphestTaskStatus::STATUS_CLOSED_INVALID,
self::STATUS_SPITE => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
self::STATUS_DUPLICATE => ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE,
);
switch ($this->status) {
case self::STATUS_ANY:
return null;
case self::STATUS_OPEN:
return qsprintf(
$conn,
'task.status IN (%Ls)',
ManiphestTaskStatus::getOpenStatusConstants());
case self::STATUS_CLOSED:
return qsprintf(
$conn,
'task.status IN (%Ls)',
ManiphestTaskStatus::getClosedStatusConstants());
default:
$constant = idx($map, $this->status);
if (!$constant) {
throw new Exception(pht("Unknown status query '%s'!", $this->status));
}
return qsprintf(
$conn,
'task.status = %s',
$constant);
}
}
private function buildOwnerWhereClause(AphrontDatabaseConnection $conn) {
$subclause = array();
if ($this->noOwner) {
$subclause[] = qsprintf(
$conn,
'task.ownerPHID IS NULL');
}
if ($this->anyOwner) {
$subclause[] = qsprintf(
$conn,
'task.ownerPHID IS NOT NULL');
}
if ($this->ownerPHIDs !== null) {
$subclause[] = qsprintf(
$conn,
'task.ownerPHID IN (%Ls)',
$this->ownerPHIDs);
}
if (!$subclause) {
return qsprintf($conn, '');
}
return qsprintf($conn, '%LO', $subclause);
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$open_statuses = ManiphestTaskStatus::getOpenStatusConstants();
$edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE;
$task_table = $this->newResultObject()->getTableName();
$parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST;
$subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST;
$joins = array();
if ($this->hasOpenParents !== null) {
if ($this->hasOpenParents) {
- $join_type = 'JOIN';
+ $join_type = qsprintf($conn, 'JOIN');
} else {
- $join_type = 'LEFT JOIN';
+ $join_type = qsprintf($conn, 'LEFT JOIN');
}
$joins[] = qsprintf(
$conn,
'%Q %T e_parent
ON e_parent.src = task.phid
AND e_parent.type = %d
%Q %T parent
ON e_parent.dst = parent.phid
AND parent.status IN (%Ls)',
$join_type,
$edge_table,
$parent_type,
$join_type,
$task_table,
$open_statuses);
}
if ($this->hasOpenSubtasks !== null) {
if ($this->hasOpenSubtasks) {
$join_type = qsprintf($conn, 'JOIN');
} else {
$join_type = qsprintf($conn, 'LEFT JOIN');
}
$joins[] = qsprintf(
$conn,
'%Q %T e_subtask
ON e_subtask.src = task.phid
AND e_subtask.type = %d
%Q %T subtask
ON e_subtask.dst = subtask.phid
AND subtask.status IN (%Ls)',
$join_type,
$edge_table,
$subtask_type,
$join_type,
$task_table,
$open_statuses);
}
if ($this->subscriberPHIDs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T e_ccs ON e_ccs.src = task.phid '.
'AND e_ccs.type = %s '.
'AND e_ccs.dst in (%Ls)',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorObjectHasSubscriberEdgeType::EDGECONST,
$this->subscriberPHIDs);
}
switch ($this->groupBy) {
case self::GROUP_PROJECT:
$ignore_group_phids = $this->getIgnoreGroupedProjectPHIDs();
if ($ignore_group_phids) {
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src
AND projectGroup.type = %d
AND projectGroup.dst NOT IN (%Ls)',
$edge_table,
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$ignore_group_phids);
} else {
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src
AND projectGroup.type = %d',
$edge_table,
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
}
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T projectGroupName
ON projectGroup.dst = projectGroupName.indexedObjectPHID',
id(new ManiphestNameIndex())->getTableName());
break;
}
if ($this->parentTaskIDs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T e_has_parent
ON e_has_parent.src = task.phid
AND e_has_parent.type = %d
JOIN %T has_parent
ON e_has_parent.dst = has_parent.phid
AND has_parent.id IN (%Ld)',
$edge_table,
$parent_type,
$task_table,
$this->parentTaskIDs);
}
if ($this->subtaskIDs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T e_has_subtask
ON e_has_subtask.src = task.phid
AND e_has_subtask.type = %d
JOIN %T has_subtask
ON e_has_subtask.dst = has_subtask.phid
AND has_subtask.id IN (%Ld)',
$edge_table,
$subtask_type,
$task_table,
$this->subtaskIDs);
}
$joins[] = parent::buildJoinClauseParts($conn);
return $joins;
}
protected function buildGroupClause(AphrontDatabaseConnection $conn) {
$joined_multiple_rows =
($this->hasOpenParents !== null) ||
($this->hasOpenSubtasks !== null) ||
($this->parentTaskIDs !== null) ||
($this->subtaskIDs !== null) ||
$this->shouldGroupQueryResultRows();
$joined_project_name = ($this->groupBy == self::GROUP_PROJECT);
// If we're joining multiple rows, we need to group the results by the
// task IDs.
if ($joined_multiple_rows) {
if ($joined_project_name) {
return qsprintf($conn, 'GROUP BY task.phid, projectGroup.dst');
} else {
return qsprintf($conn, 'GROUP BY task.phid');
}
}
return qsprintf($conn, '');
}
protected function buildHavingClauseParts(AphrontDatabaseConnection $conn) {
$having = parent::buildHavingClauseParts($conn);
if ($this->hasOpenParents !== null) {
if (!$this->hasOpenParents) {
$having[] = qsprintf(
$conn,
'COUNT(parent.phid) = 0');
}
}
if ($this->hasOpenSubtasks !== null) {
if (!$this->hasOpenSubtasks) {
$having[] = qsprintf(
$conn,
'COUNT(subtask.phid) = 0');
}
}
return $having;
}
/**
* Return project PHIDs which we should ignore when grouping tasks by
* project. For example, if a user issues a query like:
*
* Tasks tagged with all projects: Frontend, Bugs
*
* ...then we don't show "Frontend" or "Bugs" groups in the result set, since
* they're meaningless as all results are in both groups.
*
* Similarly, for queries like:
*
* Tasks tagged with any projects: Public Relations
*
* ...we ignore the single project, as every result is in that project. (In
* the case that there are several "any" projects, we do not ignore them.)
*
* @return list<phid> Project PHIDs which should be ignored in query
* construction.
*/
private function getIgnoreGroupedProjectPHIDs() {
// Maybe we should also exclude the "OPERATOR_NOT" PHIDs? It won't
// impact the results, but we might end up with a better query plan.
// Investigate this on real data? This is likely very rare.
$edge_types = array(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
);
$phids = array();
$phids[] = $this->getEdgeLogicValues(
$edge_types,
array(
PhabricatorQueryConstraint::OPERATOR_AND,
));
$any = $this->getEdgeLogicValues(
$edge_types,
array(
PhabricatorQueryConstraint::OPERATOR_OR,
));
if (count($any) == 1) {
$phids[] = $any;
}
return array_mergev($phids);
}
- protected function getResultCursor($result) {
- $id = $result->getID();
-
- if ($this->groupBy == self::GROUP_PROJECT) {
- return rtrim($id.'.'.$result->getGroupByProjectPHID(), '.');
- }
-
- return $id;
- }
-
public function getBuiltinOrders() {
$orders = array(
'priority' => array(
- 'vector' => array('priority', 'subpriority', 'id'),
+ 'vector' => array('priority', 'id'),
'name' => pht('Priority'),
'aliases' => array(self::ORDER_PRIORITY),
),
'updated' => array(
'vector' => array('updated', 'id'),
'name' => pht('Date Updated (Latest First)'),
'aliases' => array(self::ORDER_MODIFIED),
),
'outdated' => array(
'vector' => array('-updated', '-id'),
'name' => pht('Date Updated (Oldest First)'),
),
'closed' => array(
'vector' => array('closed', 'id'),
'name' => pht('Date Closed (Latest First)'),
),
'title' => array(
'vector' => array('title', 'id'),
'name' => pht('Title'),
'aliases' => array(self::ORDER_TITLE),
),
) + parent::getBuiltinOrders();
// Alias the "newest" builtin to the historical key for it.
$orders['newest']['aliases'][] = self::ORDER_CREATED;
$orders = array_select_keys(
$orders,
array(
'priority',
'updated',
'outdated',
'newest',
'oldest',
'closed',
'title',
)) + $orders;
return $orders;
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'priority' => array(
'table' => 'task',
'column' => 'priority',
'type' => 'int',
),
'owner' => array(
'table' => 'task',
'column' => 'ownerOrdering',
'null' => 'head',
'reverse' => true,
'type' => 'string',
),
'status' => array(
'table' => 'task',
'column' => 'status',
'type' => 'string',
'reverse' => true,
),
'project' => array(
'table' => 'projectGroupName',
'column' => 'indexedObjectName',
'type' => 'string',
'null' => 'head',
'reverse' => true,
),
'title' => array(
'table' => 'task',
'column' => 'title',
'type' => 'string',
'reverse' => true,
),
- 'subpriority' => array(
- 'table' => 'task',
- 'column' => 'subpriority',
- 'type' => 'float',
- ),
'updated' => array(
'table' => 'task',
'column' => 'dateModified',
'type' => 'int',
),
'closed' => array(
'table' => 'task',
'column' => 'closedEpoch',
'type' => 'int',
'null' => 'tail',
),
);
}
- protected function getPagingValueMap($cursor, array $keys) {
- $cursor_parts = explode('.', $cursor, 2);
- $task_id = $cursor_parts[0];
- $group_id = idx($cursor_parts, 1);
+ protected function newPagingMapFromCursorObject(
+ PhabricatorQueryCursor $cursor,
+ array $keys) {
- $task = $this->loadCursorObject($task_id);
+ $task = $cursor->getObject();
$map = array(
- 'id' => $task->getID(),
- 'priority' => $task->getPriority(),
- 'subpriority' => $task->getSubpriority(),
+ 'id' => (int)$task->getID(),
+ 'priority' => (int)$task->getPriority(),
'owner' => $task->getOwnerOrdering(),
'status' => $task->getStatus(),
'title' => $task->getTitle(),
- 'updated' => $task->getDateModified(),
+ 'updated' => (int)$task->getDateModified(),
'closed' => $task->getClosedEpoch(),
);
- foreach ($keys as $key) {
- switch ($key) {
- case 'project':
- $value = null;
- if ($group_id) {
- $paging_projects = id(new PhabricatorProjectQuery())
- ->setViewer($this->getViewer())
- ->withPHIDs(array($group_id))
- ->execute();
- if ($paging_projects) {
- $value = head($paging_projects)->getName();
- }
- }
- $map[$key] = $value;
- break;
+ if (isset($keys['project'])) {
+ $value = null;
+
+ $group_phid = $task->getGroupByProjectPHID();
+ if ($group_phid) {
+ $paging_projects = id(new PhabricatorProjectQuery())
+ ->setViewer($this->getViewer())
+ ->withPHIDs(array($group_phid))
+ ->execute();
+ if ($paging_projects) {
+ $value = head($paging_projects)->getName();
+ }
}
+
+ $map['project'] = $value;
}
foreach ($keys as $key) {
if ($this->isCustomFieldOrderKey($key)) {
$map += $this->getPagingValueMapForCustomFields($task);
break;
}
}
return $map;
}
+ protected function newExternalCursorStringForResult($object) {
+ $id = $object->getID();
+
+ if ($this->groupBy == self::GROUP_PROJECT) {
+ return rtrim($id.'.'.$object->getGroupByProjectPHID(), '.');
+ }
+
+ return $id;
+ }
+
+ protected function newInternalCursorFromExternalCursor($cursor) {
+ list($task_id, $group_phid) = $this->parseCursor($cursor);
+
+ $cursor_object = parent::newInternalCursorFromExternalCursor($cursor);
+
+ if ($group_phid !== null) {
+ $project = id(new PhabricatorProjectQuery())
+ ->setViewer($this->getViewer())
+ ->withPHIDs(array($group_phid))
+ ->execute();
+
+ if (!$project) {
+ $this->throwCursorException(
+ pht(
+ 'Group PHID ("%s") component of cursor ("%s") is not valid.',
+ $group_phid,
+ $cursor));
+ }
+
+ $cursor_object->getObject()->attachGroupByProjectPHID($group_phid);
+ }
+
+ return $cursor_object;
+ }
+
+ protected function applyExternalCursorConstraintsToQuery(
+ PhabricatorCursorPagedPolicyAwareQuery $subquery,
+ $cursor) {
+ list($task_id, $group_phid) = $this->parseCursor($cursor);
+
+ $subquery->withIDs(array($task_id));
+
+ if ($group_phid) {
+ $subquery->setGroupBy(self::GROUP_PROJECT);
+
+ // The subquery needs to return exactly one result. If a task is in
+ // several projects, the query may naturally return several results.
+ // Specify that we want only the particular instance of the task in
+ // the specified project.
+ $subquery->withSpecificGroupByProjectPHID($group_phid);
+ }
+ }
+
+
+ private function parseCursor($cursor) {
+ // Split a "123.PHID-PROJ-abcd" cursor into a "Task ID" part and a
+ // "Project PHID" part.
+
+ $parts = explode('.', $cursor, 2);
+
+ if (count($parts) < 2) {
+ $parts[] = null;
+ }
+
+ if (!strlen($parts[1])) {
+ $parts[1] = null;
+ }
+
+ return $parts;
+ }
+
protected function getPrimaryTableAlias() {
return 'task';
}
public function getQueryApplicationClass() {
return 'PhabricatorManiphestApplication';
}
}
diff --git a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
index f4a1f2b34..4c69c604e 100644
--- a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
+++ b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
@@ -1,588 +1,585 @@
<?php
final class ManiphestTaskSearchEngine
extends PhabricatorApplicationSearchEngine {
private $showBatchControls;
private $baseURI;
private $isBoardView;
public function setIsBoardView($is_board_view) {
$this->isBoardView = $is_board_view;
return $this;
}
public function getIsBoardView() {
return $this->isBoardView;
}
public function setBaseURI($base_uri) {
$this->baseURI = $base_uri;
return $this;
}
public function getBaseURI() {
return $this->baseURI;
}
public function setShowBatchControls($show_batch_controls) {
$this->showBatchControls = $show_batch_controls;
return $this;
}
public function getResultTypeDescription() {
return pht('Maniphest Tasks');
}
public function getApplicationClassName() {
return 'PhabricatorManiphestApplication';
}
public function newQuery() {
return id(new ManiphestTaskQuery())
->needProjectPHIDs(true);
}
protected function buildCustomSearchFields() {
// Hide the "Subtypes" constraint from the web UI if the install only
// defines one task subtype, since it isn't of any use in this case.
$subtype_map = id(new ManiphestTask())->newEditEngineSubtypeMap();
$hide_subtypes = ($subtype_map->getCount() == 1);
return array(
id(new PhabricatorOwnersSearchField())
->setLabel(pht('Assigned To'))
->setKey('assignedPHIDs')
->setConduitKey('assigned')
->setAliases(array('assigned'))
->setDescription(
pht('Search for tasks owned by a user from a list.')),
id(new PhabricatorUsersSearchField())
->setLabel(pht('Authors'))
->setKey('authorPHIDs')
->setAliases(array('author', 'authors'))
->setDescription(
pht('Search for tasks with given authors.')),
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Statuses'))
->setKey('statuses')
->setAliases(array('status'))
->setDescription(
pht('Search for tasks with given statuses.'))
->setDatasource(new ManiphestTaskStatusFunctionDatasource()),
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Priorities'))
->setKey('priorities')
->setAliases(array('priority'))
->setDescription(
pht('Search for tasks with given priorities.'))
->setConduitParameterType(new ConduitIntListParameterType())
->setDatasource(new ManiphestTaskPriorityDatasource()),
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Subtypes'))
->setKey('subtypes')
->setAliases(array('subtype'))
->setDescription(
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')
->setAliases(array('blocking'))
->setOptions(
pht('(Show All)'),
pht('Show Only Tasks With Open Parents'),
pht('Show Only Tasks Without Open Parents')),
id(new PhabricatorSearchThreeStateField())
->setLabel(pht('Open Subtasks'))
->setKey('hasSubtasks')
->setAliases(array('blocked'))
->setOptions(
pht('(Show All)'),
pht('Show Only Tasks With Open Subtasks'),
pht('Show Only Tasks Without Open Subtasks')),
id(new PhabricatorIDsSearchField())
->setLabel(pht('Parent IDs'))
->setKey('parentIDs')
->setAliases(array('parentID')),
id(new PhabricatorIDsSearchField())
->setLabel(pht('Subtask IDs'))
->setKey('subtaskIDs')
->setAliases(array('subtaskID')),
id(new PhabricatorSearchSelectField())
->setLabel(pht('Group By'))
->setKey('group')
->setOptions($this->getGroupOptions()),
id(new PhabricatorSearchDateField())
->setLabel(pht('Created After'))
->setKey('createdStart'),
id(new PhabricatorSearchDateField())
->setLabel(pht('Created Before'))
->setKey('createdEnd'),
id(new PhabricatorSearchDateField())
->setLabel(pht('Updated After'))
->setKey('modifiedStart'),
id(new PhabricatorSearchDateField())
->setLabel(pht('Updated Before'))
->setKey('modifiedEnd'),
id(new PhabricatorSearchDateField())
->setLabel(pht('Closed After'))
->setKey('closedStart'),
id(new PhabricatorSearchDateField())
->setLabel(pht('Closed Before'))
->setKey('closedEnd'),
id(new PhabricatorUsersSearchField())
->setLabel(pht('Closed By'))
->setKey('closerPHIDs')
->setAliases(array('closer', 'closerPHID', 'closers'))
->setDescription(pht('Search for tasks closed by certain users.')),
id(new PhabricatorSearchTextField())
->setLabel(pht('Page Size'))
->setKey('limit'),
);
}
protected function getDefaultFieldOrder() {
return array(
'assignedPHIDs',
'projectPHIDs',
'authorPHIDs',
'subscriberPHIDs',
'statuses',
'priorities',
'subtypes',
'hasParents',
'hasSubtasks',
'parentIDs',
'subtaskIDs',
'group',
'order',
'ids',
'...',
'createdStart',
'createdEnd',
'modifiedStart',
'modifiedEnd',
'closedStart',
'closedEnd',
'closerPHIDs',
'limit',
);
}
protected function getHiddenFields() {
$keys = array();
if ($this->getIsBoardView()) {
$keys[] = 'group';
$keys[] = 'order';
$keys[] = 'limit';
}
return $keys;
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
if ($map['assignedPHIDs']) {
$query->withOwners($map['assignedPHIDs']);
}
if ($map['authorPHIDs']) {
$query->withAuthors($map['authorPHIDs']);
}
if ($map['statuses']) {
$query->withStatuses($map['statuses']);
}
if ($map['priorities']) {
$query->withPriorities($map['priorities']);
}
if ($map['subtypes']) {
$query->withSubtypes($map['subtypes']);
}
if ($map['createdStart']) {
$query->withDateCreatedAfter($map['createdStart']);
}
if ($map['createdEnd']) {
$query->withDateCreatedBefore($map['createdEnd']);
}
if ($map['modifiedStart']) {
$query->withDateModifiedAfter($map['modifiedStart']);
}
if ($map['modifiedEnd']) {
$query->withDateModifiedBefore($map['modifiedEnd']);
}
if ($map['closedStart'] || $map['closedEnd']) {
$query->withClosedEpochBetween($map['closedStart'], $map['closedEnd']);
}
if ($map['closerPHIDs']) {
$query->withCloserPHIDs($map['closerPHIDs']);
}
if ($map['hasParents'] !== null) {
$query->withOpenParents($map['hasParents']);
}
if ($map['hasSubtasks'] !== null) {
$query->withOpenSubtasks($map['hasSubtasks']);
}
if ($map['parentIDs']) {
$query->withParentTaskIDs($map['parentIDs']);
}
if ($map['subtaskIDs']) {
$query->withSubtaskIDs($map['subtaskIDs']);
}
if ($map['columnPHIDs']) {
$query->withColumnPHIDs($map['columnPHIDs']);
}
$group = idx($map, 'group');
$group = idx($this->getGroupValues(), $group);
if ($group) {
$query->setGroupBy($group);
}
if ($map['ids']) {
$ids = $map['ids'];
foreach ($ids as $key => $id) {
$id = trim($id, ' Tt');
if (!$id || !is_numeric($id)) {
unset($ids[$key]);
} else {
$ids[$key] = $id;
}
}
if ($ids) {
$query->withIDs($ids);
}
}
return $query;
}
protected function getURI($path) {
if ($this->baseURI) {
return $this->baseURI.$path;
}
return '/maniphest/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array();
if ($this->requireViewer()->isLoggedIn()) {
$names['assigned'] = pht('Assigned');
$names['authored'] = pht('Authored');
$names['subscribed'] = pht('Subscribed');
}
$names['open'] = pht('Open Tasks');
$names['all'] = pht('All Tasks');
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
$viewer_phid = $this->requireViewer()->getPHID();
switch ($query_key) {
case 'all':
return $query;
case 'assigned':
return $query
->setParameter('assignedPHIDs', array($viewer_phid))
->setParameter(
'statuses',
ManiphestTaskStatus::getOpenStatusConstants());
case 'subscribed':
return $query
->setParameter('subscriberPHIDs', array($viewer_phid))
->setParameter(
'statuses',
ManiphestTaskStatus::getOpenStatusConstants());
case 'open':
return $query
->setParameter(
'statuses',
ManiphestTaskStatus::getOpenStatusConstants());
case 'authored':
return $query
->setParameter('authorPHIDs', array($viewer_phid))
->setParameter('order', 'created')
->setParameter('group', 'none');
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
private function getGroupOptions() {
return array(
'priority' => pht('Priority'),
'assigned' => pht('Assigned'),
'status' => pht('Status'),
'project' => pht('Project'),
'none' => pht('None'),
);
}
private function getGroupValues() {
return array(
'priority' => ManiphestTaskQuery::GROUP_PRIORITY,
'assigned' => ManiphestTaskQuery::GROUP_OWNER,
'status' => ManiphestTaskQuery::GROUP_STATUS,
'project' => ManiphestTaskQuery::GROUP_PROJECT,
'none' => ManiphestTaskQuery::GROUP_NONE,
);
}
protected function renderResultList(
array $tasks,
PhabricatorSavedQuery $saved,
array $handles) {
$viewer = $this->requireViewer();
if ($this->isPanelContext()) {
- $can_edit_priority = false;
$can_bulk_edit = false;
} else {
- $can_edit_priority = true;
$can_bulk_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$this->getApplication(),
ManiphestBulkEditCapability::CAPABILITY);
}
$list = id(new ManiphestTaskResultListView())
->setUser($viewer)
->setTasks($tasks)
->setSavedQuery($saved)
- ->setCanEditPriority($can_edit_priority)
->setCanBatchEdit($can_bulk_edit)
->setShowBatchControls($this->showBatchControls);
$result = new PhabricatorApplicationSearchResultView();
$result->setContent($list);
return $result;
}
protected function willUseSavedQuery(PhabricatorSavedQuery $saved) {
// The 'withUnassigned' parameter may be present in old saved queries from
// before parameterized typeaheads, and is retained for compatibility. We
// could remove it by migrating old saved queries.
$assigned_phids = $saved->getParameter('assignedPHIDs', array());
if ($saved->getParameter('withUnassigned')) {
$assigned_phids[] = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN;
}
$saved->setParameter('assignedPHIDs', $assigned_phids);
// The 'projects' and other parameters may be present in old saved queries
// from before parameterized typeaheads.
$project_phids = $saved->getParameter('projectPHIDs', array());
$old = $saved->getParameter('projects', array());
foreach ($old as $phid) {
$project_phids[] = $phid;
}
$all = $saved->getParameter('allProjectPHIDs', array());
foreach ($all as $phid) {
$project_phids[] = $phid;
}
$any = $saved->getParameter('anyProjectPHIDs', array());
foreach ($any as $phid) {
$project_phids[] = 'any('.$phid.')';
}
$not = $saved->getParameter('excludeProjectPHIDs', array());
foreach ($not as $phid) {
$project_phids[] = 'not('.$phid.')';
}
$users = $saved->getParameter('userProjectPHIDs', array());
foreach ($users as $phid) {
$project_phids[] = 'projects('.$phid.')';
}
$no = $saved->getParameter('withNoProject');
if ($no) {
$project_phids[] = 'null()';
}
$saved->setParameter('projectPHIDs', $project_phids);
}
protected function getNewUserBody() {
$viewer = $this->requireViewer();
$create_button = id(new ManiphestEditEngine())
->setViewer($viewer)
->newNUXBUtton(pht('Create a Task'));
$icon = $this->getApplication()->getIcon();
$app_name = $this->getApplication()->getName();
$view = id(new PHUIBigInfoView())
->setIcon($icon)
->setTitle(pht('Welcome to %s', $app_name))
->setDescription(
pht('Use Maniphest to track bugs, features, todos, or anything else '.
'you need to get done. Tasks assigned to you will appear here.'))
->addAction($create_button);
return $view;
}
protected function newExportFields() {
$fields = array(
id(new PhabricatorStringExportField())
->setKey('monogram')
->setLabel(pht('Monogram')),
id(new PhabricatorPHIDExportField())
->setKey('authorPHID')
->setLabel(pht('Author PHID')),
id(new PhabricatorStringExportField())
->setKey('author')
->setLabel(pht('Author')),
id(new PhabricatorPHIDExportField())
->setKey('ownerPHID')
->setLabel(pht('Owner PHID')),
id(new PhabricatorStringExportField())
->setKey('owner')
->setLabel(pht('Owner')),
id(new PhabricatorStringExportField())
->setKey('status')
->setLabel(pht('Status')),
id(new PhabricatorStringExportField())
->setKey('statusName')
->setLabel(pht('Status Name')),
id(new PhabricatorEpochExportField())
->setKey('dateClosed')
->setLabel(pht('Date Closed')),
id(new PhabricatorPHIDExportField())
->setKey('closerPHID')
->setLabel(pht('Closer PHID')),
id(new PhabricatorStringExportField())
->setKey('closer')
->setLabel(pht('Closer')),
id(new PhabricatorStringExportField())
->setKey('priority')
->setLabel(pht('Priority')),
id(new PhabricatorStringExportField())
->setKey('priorityName')
->setLabel(pht('Priority Name')),
id(new PhabricatorStringExportField())
->setKey('subtype')
->setLabel('Subtype'),
id(new PhabricatorURIExportField())
->setKey('uri')
->setLabel(pht('URI')),
id(new PhabricatorStringExportField())
->setKey('title')
->setLabel(pht('Title')),
id(new PhabricatorStringExportField())
->setKey('description')
->setLabel(pht('Description')),
);
if (ManiphestTaskPoints::getIsEnabled()) {
$fields[] = id(new PhabricatorDoubleExportField())
->setKey('points')
->setLabel('Points');
}
return $fields;
}
protected function newExportData(array $tasks) {
$viewer = $this->requireViewer();
$phids = array();
foreach ($tasks as $task) {
$phids[] = $task->getAuthorPHID();
$phids[] = $task->getOwnerPHID();
$phids[] = $task->getCloserPHID();
}
$handles = $viewer->loadHandles($phids);
$export = array();
foreach ($tasks as $task) {
$author_phid = $task->getAuthorPHID();
if ($author_phid) {
$author_name = $handles[$author_phid]->getName();
} else {
$author_name = null;
}
$owner_phid = $task->getOwnerPHID();
if ($owner_phid) {
$owner_name = $handles[$owner_phid]->getName();
} else {
$owner_name = null;
}
$closer_phid = $task->getCloserPHID();
if ($closer_phid) {
$closer_name = $handles[$closer_phid]->getName();
} else {
$closer_name = null;
}
$status_value = $task->getStatus();
$status_name = ManiphestTaskStatus::getTaskStatusName($status_value);
$priority_value = $task->getPriority();
$priority_name = ManiphestTaskPriority::getTaskPriorityName(
$priority_value);
$export[] = array(
'monogram' => $task->getMonogram(),
'authorPHID' => $author_phid,
'author' => $author_name,
'ownerPHID' => $owner_phid,
'owner' => $owner_name,
'status' => $status_value,
'statusName' => $status_name,
'priority' => $priority_value,
'priorityName' => $priority_name,
'points' => $task->getPoints(),
'subtype' => $task->getSubtype(),
'title' => $task->getTitle(),
'uri' => PhabricatorEnv::getProductionURI($task->getURI()),
'description' => $task->getDescription(),
'dateClosed' => $task->getClosedEpoch(),
'closerPHID' => $closer_phid,
'closer' => $closer_name,
);
}
return $export;
}
}
diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php
index ada537fa4..d2700895c 100644
--- a/src/applications/maniphest/storage/ManiphestTask.php
+++ b/src/applications/maniphest/storage/ManiphestTask.php
@@ -1,631 +1,610 @@
<?php
final class ManiphestTask extends ManiphestDAO
implements
PhabricatorSubscribableInterface,
PhabricatorMarkupInterface,
PhabricatorPolicyInterface,
PhabricatorTokenReceiverInterface,
PhabricatorFlaggableInterface,
PhabricatorMentionableInterface,
PhrequentTrackableInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorProjectInterface,
PhabricatorSpacesInterface,
PhabricatorConduitResultInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
DoorkeeperBridgedObjectInterface,
PhabricatorEditEngineSubtypeInterface,
PhabricatorEditEngineLockableInterface,
- PhabricatorEditEngineMFAInterface {
+ PhabricatorEditEngineMFAInterface,
+ PhabricatorPolicyCodexInterface,
+ PhabricatorUnlockableInterface {
const MARKUP_FIELD_DESCRIPTION = 'markup:desc';
protected $authorPHID;
protected $ownerPHID;
protected $status;
protected $priority;
protected $subpriority = 0;
protected $title = '';
protected $description = '';
protected $originalEmailSource;
protected $mailKey;
protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
protected $editPolicy = PhabricatorPolicies::POLICY_USER;
protected $ownerOrdering;
protected $spacePHID;
protected $bridgedObjectPHID;
protected $properties = array();
protected $points;
protected $subtype;
protected $closedEpoch;
protected $closerPHID;
private $subscriberPHIDs = self::ATTACHABLE;
private $groupByProjectPHID = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $edgeProjectPHIDs = self::ATTACHABLE;
private $bridgedObject = self::ATTACHABLE;
public static function initializeNewTask(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorManiphestApplication'))
->executeOne();
$view_policy = $app->getPolicy(ManiphestDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(ManiphestDefaultEditCapability::CAPABILITY);
return id(new ManiphestTask())
->setStatus(ManiphestTaskStatus::getDefaultStatus())
->setPriority(ManiphestTaskPriority::getDefaultPriority())
->setAuthorPHID($actor->getPHID())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setSpacePHID($actor->getDefaultSpacePHID())
->setSubtype(PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT)
->attachProjectPHIDs(array())
->attachSubscriberPHIDs(array());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'ownerPHID' => 'phid?',
'status' => 'text64',
'priority' => 'uint32',
'title' => 'sort',
'description' => 'text',
'mailKey' => 'bytes20',
'ownerOrdering' => 'text64?',
'originalEmailSource' => 'text255?',
'subpriority' => 'double',
'points' => 'double?',
'bridgedObjectPHID' => 'phid?',
'subtype' => 'text64',
'closedEpoch' => 'epoch?',
'closerPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'priority' => array(
'columns' => array('priority', 'status'),
),
'status' => array(
'columns' => array('status'),
),
'ownerPHID' => array(
'columns' => array('ownerPHID', 'status'),
),
'authorPHID' => array(
'columns' => array('authorPHID', 'status'),
),
'ownerOrdering' => array(
'columns' => array('ownerOrdering'),
),
'priority_2' => array(
'columns' => array('priority', 'subpriority'),
),
'key_dateCreated' => array(
'columns' => array('dateCreated'),
),
'key_dateModified' => array(
'columns' => array('dateModified'),
),
'key_title' => array(
'columns' => array('title(64)'),
),
'key_bridgedobject' => array(
'columns' => array('bridgedObjectPHID'),
'unique' => true,
),
'key_subtype' => array(
'columns' => array('subtype'),
),
'key_closed' => array(
'columns' => array('closedEpoch'),
),
'key_closer' => array(
'columns' => array('closerPHID', 'closedEpoch'),
),
),
) + parent::getConfiguration();
}
public function loadDependsOnTaskPHIDs() {
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(),
ManiphestTaskDependsOnTaskEdgeType::EDGECONST);
}
public function loadDependedOnByTaskPHIDs() {
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(),
ManiphestTaskDependedOnByTaskEdgeType::EDGECONST);
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(ManiphestTaskPHIDType::TYPECONST);
}
public function getSubscriberPHIDs() {
return $this->assertAttached($this->subscriberPHIDs);
}
public function getProjectPHIDs() {
return $this->assertAttached($this->edgeProjectPHIDs);
}
public function attachProjectPHIDs(array $phids) {
$this->edgeProjectPHIDs = $phids;
return $this;
}
public function attachSubscriberPHIDs(array $phids) {
$this->subscriberPHIDs = $phids;
return $this;
}
public function setOwnerPHID($phid) {
$this->ownerPHID = nonempty($phid, null);
return $this;
}
public function getMonogram() {
return 'T'.$this->getID();
}
public function getURI() {
return '/'.$this->getMonogram();
}
public function attachGroupByProjectPHID($phid) {
$this->groupByProjectPHID = $phid;
return $this;
}
public function getGroupByProjectPHID() {
return $this->assertAttached($this->groupByProjectPHID);
}
public function save() {
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
$result = parent::save();
return $result;
}
public function isClosed() {
return ManiphestTaskStatus::isClosedStatus($this->getStatus());
}
- public function isLocked() {
- return ManiphestTaskStatus::isLockedStatus($this->getStatus());
+ public function areCommentsLocked() {
+ if ($this->areEditsLocked()) {
+ return true;
+ }
+
+ return ManiphestTaskStatus::areCommentsLockedInStatus($this->getStatus());
+ }
+
+ public function areEditsLocked() {
+ return ManiphestTaskStatus::areEditsLockedInStatus($this->getStatus());
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function getCoverImageFilePHID() {
return idx($this->properties, 'cover.filePHID');
}
public function getCoverImageThumbnailPHID() {
return idx($this->properties, 'cover.thumbnailPHID');
}
- public function getWorkboardOrderVectors() {
- return array(
- PhabricatorProjectColumn::ORDER_PRIORITY => array(
- (int)-$this->getPriority(),
- (double)-$this->getSubpriority(),
- (int)-$this->getID(),
- ),
- );
- }
-
public function getPriorityKeyword() {
$priority = $this->getPriority();
$keyword = ManiphestTaskPriority::getKeywordForTaskPriority($priority);
if ($keyword !== null) {
return $keyword;
}
return ManiphestTaskPriority::UNKNOWN_PRIORITY_KEYWORD;
}
- private function comparePriorityTo(ManiphestTask $other) {
- $upri = $this->getPriority();
- $vpri = $other->getPriority();
-
- if ($upri != $vpri) {
- return ($upri - $vpri);
- }
-
- $usub = $this->getSubpriority();
- $vsub = $other->getSubpriority();
-
- if ($usub != $vsub) {
- return ($usub - $vsub);
- }
-
- $uid = $this->getID();
- $vid = $other->getID();
-
- if ($uid != $vid) {
- return ($uid - $vid);
- }
-
- return 0;
- }
-
- public function isLowerPriorityThan(ManiphestTask $other) {
- return ($this->comparePriorityTo($other) < 0);
- }
-
- public function isHigherPriorityThan(ManiphestTask $other) {
- return ($this->comparePriorityTo($other) > 0);
- }
-
- public function getWorkboardProperties() {
- return array(
- 'status' => $this->getStatus(),
- 'points' => (double)$this->getPoints(),
- );
- }
-
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($phid == $this->getOwnerPHID());
}
/* -( Markup Interface )--------------------------------------------------- */
/**
* @task markup
*/
public function getMarkupFieldKey($field) {
$content = $this->getMarkupText($field);
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}
/**
* @task markup
*/
public function getMarkupText($field) {
return $this->getDescription();
}
/**
* @task markup
*/
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newManiphestMarkupEngine();
}
/**
* @task markup
*/
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
/**
* @task markup
*/
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
/* -( Policy Interface )--------------------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_INTERACT,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_INTERACT:
- if ($this->isLocked()) {
+ if ($this->areCommentsLocked()) {
return PhabricatorPolicies::POLICY_NOONE;
} else {
return $this->getViewPolicy();
}
case PhabricatorPolicyCapability::CAN_EDIT:
- return $this->getEditPolicy();
+ if ($this->areEditsLocked()) {
+ return PhabricatorPolicies::POLICY_NOONE;
+ } else {
+ return $this->getEditPolicy();
+ }
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
// The owner of a task can always view and edit it.
$owner_phid = $this->getOwnerPHID();
if ($owner_phid) {
$user_phid = $user->getPHID();
if ($user_phid == $owner_phid) {
return true;
}
}
return false;
}
public function describeAutomaticCapability($capability) {
return pht('The owner of a task can always view and edit it.');
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
// Sort of ambiguous who this was intended for; just let them both know.
return array_filter(
array_unique(
array(
$this->getAuthorPHID(),
$this->getOwnerPHID(),
)));
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('maniphest.fields');
}
public function getCustomFieldBaseClass() {
return 'ManiphestCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new ManiphestTransactionEditor();
}
public function getApplicationTransactionTemplate() {
return new ManiphestTransaction();
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
return $this->spacePHID;
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('title')
->setType('string')
->setDescription(pht('The title of the task.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('description')
->setType('remarkup')
->setDescription(pht('The task description.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('authorPHID')
->setType('phid')
->setDescription(pht('Original task author.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('ownerPHID')
->setType('phid?')
->setDescription(pht('Current task owner, if task is assigned.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('status')
->setType('map<string, wild>')
->setDescription(pht('Information about task status.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('priority')
->setType('map<string, wild>')
->setDescription(pht('Information about task priority.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('points')
->setType('points')
->setDescription(pht('Point value of the task.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('subtype')
->setType('string')
->setDescription(pht('Subtype of the task.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('closerPHID')
->setType('phid?')
->setDescription(
pht('User who closed the task, if the task is closed.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('dateClosed')
->setType('int?')
->setDescription(
pht('Epoch timestamp when the task was closed.')),
);
}
public function getFieldValuesForConduit() {
$status_value = $this->getStatus();
$status_info = array(
'value' => $status_value,
'name' => ManiphestTaskStatus::getTaskStatusName($status_value),
'color' => ManiphestTaskStatus::getStatusColor($status_value),
);
$priority_value = (int)$this->getPriority();
$priority_info = array(
'value' => $priority_value,
- 'subpriority' => (double)$this->getSubpriority(),
'name' => ManiphestTaskPriority::getTaskPriorityName($priority_value),
'color' => ManiphestTaskPriority::getTaskPriorityColor($priority_value),
);
$closed_epoch = $this->getClosedEpoch();
if ($closed_epoch !== null) {
$closed_epoch = (int)$closed_epoch;
}
return array(
'name' => $this->getTitle(),
'description' => array(
'raw' => $this->getDescription(),
),
'authorPHID' => $this->getAuthorPHID(),
'ownerPHID' => $this->getOwnerPHID(),
'status' => $status_info,
'priority' => $priority_info,
'points' => $this->getPoints(),
'subtype' => $this->getSubtype(),
'closerPHID' => $this->getCloserPHID(),
'dateClosed' => $closed_epoch,
);
}
public function getConduitSearchAttachments() {
return array(
id(new PhabricatorBoardColumnsSearchEngineAttachment())
->setAttachmentKey('columns'),
);
}
public function newSubtypeObject() {
$subtype_key = $this->getEditEngineSubtype();
$subtype_map = $this->newEditEngineSubtypeMap();
return $subtype_map->getSubtype($subtype_key);
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new ManiphestTaskFulltextEngine();
}
/* -( DoorkeeperBridgedObjectInterface )----------------------------------- */
public function getBridgedObject() {
return $this->assertAttached($this->bridgedObject);
}
public function attachBridgedObject(
DoorkeeperExternalObject $object = null) {
$this->bridgedObject = $object;
return $this;
}
/* -( PhabricatorEditEngineSubtypeInterface )------------------------------ */
public function getEditEngineSubtype() {
return $this->getSubtype();
}
public function setEditEngineSubtype($value) {
return $this->setSubtype($value);
}
public function newEditEngineSubtypeMap() {
$config = PhabricatorEnv::getEnvConfig('maniphest.subtypes');
return PhabricatorEditEngineSubtype::newSubtypeMap($config);
}
/* -( PhabricatorEditEngineLockableInterface )----------------------------- */
public function newEditEngineLock() {
return new ManiphestTaskEditEngineLock();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new ManiphestTaskFerretEngine();
}
/* -( PhabricatorEditEngineMFAInterface )---------------------------------- */
public function newEditEngineMFAEngine() {
return new ManiphestTaskMFAEngine();
}
+
+/* -( PhabricatorPolicyCodexInterface )------------------------------------ */
+
+
+ public function newPolicyCodex() {
+ return new ManiphestTaskPolicyCodex();
+ }
+
+
+/* -( PhabricatorUnlockableInterface )------------------------------------- */
+
+
+ public function newUnlockEngine() {
+ return new ManiphestTaskUnlockEngine();
+ }
+
}
diff --git a/src/applications/maniphest/view/ManiphestTaskListView.php b/src/applications/maniphest/view/ManiphestTaskListView.php
index e7128fb85..f9ad9e604 100644
--- a/src/applications/maniphest/view/ManiphestTaskListView.php
+++ b/src/applications/maniphest/view/ManiphestTaskListView.php
@@ -1,179 +1,167 @@
<?php
final class ManiphestTaskListView extends ManiphestView {
private $tasks;
private $handles;
private $showBatchControls;
- private $showSubpriorityControls;
private $noDataString;
public function setTasks(array $tasks) {
assert_instances_of($tasks, 'ManiphestTask');
$this->tasks = $tasks;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setShowBatchControls($show_batch_controls) {
$this->showBatchControls = $show_batch_controls;
return $this;
}
- public function setShowSubpriorityControls($show_subpriority_controls) {
- $this->showSubpriorityControls = $show_subpriority_controls;
- return $this;
- }
-
public function setNoDataString($text) {
$this->noDataString = $text;
return $this;
}
public function render() {
$handles = $this->handles;
require_celerity_resource('maniphest-task-summary-css');
$list = new PHUIObjectItemListView();
if ($this->noDataString) {
$list->setNoDataString($this->noDataString);
} else {
$list->setNoDataString(pht('No tasks.'));
}
$status_map = ManiphestTaskStatus::getTaskStatusMap();
$color_map = ManiphestTaskPriority::getColorMap();
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
if ($this->showBatchControls) {
Javelin::initBehavior('maniphest-list-editor');
}
foreach ($this->tasks as $task) {
$item = id(new PHUIObjectItemView())
->setUser($this->getUser())
->setObject($task)
->setObjectName('T'.$task->getID())
->setHeader($task->getTitle())
->setHref('/T'.$task->getID());
if ($task->getOwnerPHID()) {
$owner = $handles[$task->getOwnerPHID()];
$item->addByline(pht('Assigned: %s', $owner->renderLink()));
}
$status = $task->getStatus();
$pri = idx($priority_map, $task->getPriority());
$status_name = idx($status_map, $task->getStatus());
$tooltip = pht('%s, %s', $status_name, $pri);
$icon = ManiphestTaskStatus::getStatusIcon($task->getStatus());
$color = idx($color_map, $task->getPriority(), 'grey');
if ($task->isClosed()) {
$item->setDisabled(true);
$color = 'grey';
}
$item->setStatusIcon($icon.' '.$color, $tooltip);
if ($task->isClosed()) {
$closed_epoch = $task->getClosedEpoch();
// We don't expect a task to be closed without a closed epoch, but
// recover if we find one. This can happen with older objects or with
// lipsum test data.
if (!$closed_epoch) {
$closed_epoch = $task->getDateModified();
}
$item->addIcon(
'fa-check-square-o grey',
phabricator_datetime($closed_epoch, $this->getUser()));
} else {
$item->addIcon(
'none',
phabricator_datetime($task->getDateModified(), $this->getUser()));
}
- if ($this->showSubpriorityControls) {
- $item->setGrippable(true);
- }
- if ($this->showSubpriorityControls || $this->showBatchControls) {
+ if ($this->showBatchControls) {
$item->addSigil('maniphest-task');
}
$subtype = $task->newSubtypeObject();
if ($subtype && $subtype->hasTagView()) {
$subtype_tag = $subtype->newTagView()
->setSlimShady(true);
$item->addAttribute($subtype_tag);
}
$project_handles = array_select_keys(
$handles,
array_reverse($task->getProjectPHIDs()));
$item->addAttribute(
id(new PHUIHandleTagListView())
->setLimit(4)
->setNoDataString(pht('No Projects'))
->setSlim(true)
->setHandles($project_handles));
$item->setMetadata(
array(
'taskID' => $task->getID(),
));
if ($this->showBatchControls) {
$href = new PhutilURI('/maniphest/task/edit/'.$task->getID().'/');
- if (!$this->showSubpriorityControls) {
- $href->setQueryParam('ungrippable', 'true');
- }
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-pencil')
->addSigil('maniphest-edit-task')
->setHref($href));
}
$list->addItem($item);
}
return $list;
}
public static function loadTaskHandles(
PhabricatorUser $viewer,
array $tasks) {
assert_instances_of($tasks, 'ManiphestTask');
$phids = array();
foreach ($tasks as $task) {
$assigned_phid = $task->getOwnerPHID();
if ($assigned_phid) {
$phids[] = $assigned_phid;
}
foreach ($task->getProjectPHIDs() as $project_phid) {
$phids[] = $project_phid;
}
}
if (!$phids) {
return array();
}
return id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs($phids)
->execute();
}
}
diff --git a/src/applications/maniphest/view/ManiphestTaskResultListView.php b/src/applications/maniphest/view/ManiphestTaskResultListView.php
index 6aafcbdcc..cc2a13529 100644
--- a/src/applications/maniphest/view/ManiphestTaskResultListView.php
+++ b/src/applications/maniphest/view/ManiphestTaskResultListView.php
@@ -1,260 +1,230 @@
<?php
final class ManiphestTaskResultListView extends ManiphestView {
private $tasks;
private $savedQuery;
- private $canEditPriority;
private $canBatchEdit;
private $showBatchControls;
public function setSavedQuery(PhabricatorSavedQuery $query) {
$this->savedQuery = $query;
return $this;
}
public function setTasks(array $tasks) {
$this->tasks = $tasks;
return $this;
}
- public function setCanEditPriority($can_edit_priority) {
- $this->canEditPriority = $can_edit_priority;
- return $this;
- }
-
public function setCanBatchEdit($can_batch_edit) {
$this->canBatchEdit = $can_batch_edit;
return $this;
}
public function setShowBatchControls($show_batch_controls) {
$this->showBatchControls = $show_batch_controls;
return $this;
}
public function render() {
$viewer = $this->getUser();
$tasks = $this->tasks;
$query = $this->savedQuery;
// If we didn't match anything, just pick up the default empty state.
if (!$tasks) {
return id(new PHUIObjectItemListView())
->setUser($viewer)
->setNoDataString(pht('No tasks found.'));
}
$group_parameter = nonempty($query->getParameter('group'), 'priority');
$order_parameter = nonempty($query->getParameter('order'), 'priority');
$handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks);
$groups = $this->groupTasks(
$tasks,
$group_parameter,
$handles);
- $can_edit_priority = $this->canEditPriority;
-
- $can_drag = ($order_parameter == 'priority') &&
- ($can_edit_priority) &&
- ($group_parameter == 'none' || $group_parameter == 'priority');
-
- if (!$viewer->isLoggedIn()) {
- // TODO: (T7131) Eventually, we conceivably need to make each task
- // draggable individually, since the user may be able to edit some but
- // not others.
- $can_drag = false;
- }
-
$result = array();
$lists = array();
foreach ($groups as $group => $list) {
$task_list = new ManiphestTaskListView();
$task_list->setShowBatchControls($this->showBatchControls);
- if ($can_drag) {
- $task_list->setShowSubpriorityControls(true);
- }
$task_list->setUser($viewer);
$task_list->setTasks($list);
$task_list->setHandles($handles);
$header = id(new PHUIHeaderView())
->addSigil('task-group')
->setMetadata(array('priority' => head($list)->getPriority()))
->setHeader(pht('%s (%s)', $group, phutil_count($list)));
$lists[] = id(new PHUIObjectBoxView())
->setHeader($header)
->setObjectList($task_list);
}
- if ($can_drag) {
- Javelin::initBehavior(
- 'maniphest-subpriority-editor',
- array(
- 'uri' => '/maniphest/subpriority/',
- ));
- }
-
return array(
$lists,
$this->showBatchControls ? $this->renderBatchEditor($query) : null,
);
}
private function groupTasks(array $tasks, $group, array $handles) {
assert_instances_of($tasks, 'ManiphestTask');
assert_instances_of($handles, 'PhabricatorObjectHandle');
$groups = $this->getTaskGrouping($tasks, $group);
$results = array();
foreach ($groups as $label_key => $tasks) {
$label = $this->getTaskLabelName($group, $label_key, $handles);
$results[$label][] = $tasks;
}
foreach ($results as $label => $task_groups) {
$results[$label] = array_mergev($task_groups);
}
return $results;
}
private function getTaskGrouping(array $tasks, $group) {
switch ($group) {
case 'priority':
return mgroup($tasks, 'getPriority');
case 'status':
return mgroup($tasks, 'getStatus');
case 'assigned':
return mgroup($tasks, 'getOwnerPHID');
case 'project':
return mgroup($tasks, 'getGroupByProjectPHID');
default:
return array(pht('Tasks') => $tasks);
}
}
private function getTaskLabelName($group, $label_key, array $handles) {
switch ($group) {
case 'priority':
return ManiphestTaskPriority::getTaskPriorityName($label_key);
case 'status':
return ManiphestTaskStatus::getTaskStatusFullName($label_key);
case 'assigned':
if ($label_key) {
return $handles[$label_key]->getFullName();
} else {
return pht('(Not Assigned)');
}
case 'project':
if ($label_key) {
return $handles[$label_key]->getFullName();
} else {
// This may mean "No Projects", or it may mean the query has project
// constraints but the task is only in constrained projects (in this
// case, we don't show the group because it would always have all
// of the tasks). Since distinguishing between these two cases is
// messy and the UI is reasonably clear, label generically.
return pht('(Ungrouped)');
}
default:
return pht('Tasks');
}
}
private function renderBatchEditor(PhabricatorSavedQuery $saved_query) {
$user = $this->getUser();
if (!$this->canBatchEdit) {
return null;
}
if (!$user->isLoggedIn()) {
// Don't show the batch editor for logged-out users.
return null;
}
Javelin::initBehavior(
'maniphest-batch-selector',
array(
'selectAll' => 'batch-select-all',
'selectNone' => 'batch-select-none',
'submit' => 'batch-select-submit',
'status' => 'batch-select-status-cell',
'idContainer' => 'batch-select-id-container',
'formID' => 'batch-select-form',
));
$select_all = javelin_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'class' => 'button button-grey',
'id' => 'batch-select-all',
),
pht('Select All'));
$select_none = javelin_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'class' => 'button button-grey',
'id' => 'batch-select-none',
),
pht('Clear Selection'));
$submit = phutil_tag(
'button',
array(
'id' => 'batch-select-submit',
'disabled' => 'disabled',
'class' => 'disabled',
),
pht("Bulk Edit Selected \xC2\xBB"));
$hidden = phutil_tag(
'div',
array(
'id' => 'batch-select-id-container',
),
'');
$editor = hsprintf(
'<table class="maniphest-batch-editor-layout">'.
'<tr>'.
'<td>%s%s</td>'.
'<td id="batch-select-status-cell">%s</td>'.
'<td class="batch-select-submit-cell">%s%s</td>'.
'</tr>'.
'</table>',
$select_all,
$select_none,
'',
$submit,
$hidden);
$editor = phabricator_form(
$user,
array(
'method' => 'POST',
'action' => '/maniphest/bulk/',
'id' => 'batch-select-form',
),
$editor);
$box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Batch Task Editor'))
->appendChild($editor);
$content = phutil_tag_div('maniphest-batch-editor', $box);
return $content;
}
}
diff --git a/src/applications/maniphest/xaction/ManiphestTaskCoverImageTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskCoverImageTransaction.php
index eb29c711f..5e6f63c5c 100644
--- a/src/applications/maniphest/xaction/ManiphestTaskCoverImageTransaction.php
+++ b/src/applications/maniphest/xaction/ManiphestTaskCoverImageTransaction.php
@@ -1,106 +1,124 @@
<?php
final class ManiphestTaskCoverImageTransaction
extends ManiphestTaskTransactionType {
const TRANSACTIONTYPE = 'cover-image';
public function generateOldValue($object) {
return $object->getCoverImageFilePHID();
}
public function applyInternalEffects($object, $value) {
$file_phid = $value;
if ($file_phid) {
$file = id(new PhabricatorFileQuery())
->setViewer($this->getActor())
->withPHIDs(array($file_phid))
->executeOne();
} else {
$file = null;
}
if (!$file || !$file->isTransformableImage()) {
$object->setProperty('cover.filePHID', null);
$object->setProperty('cover.thumbnailPHID', null);
return;
}
$xform_key = PhabricatorFileThumbnailTransform::TRANSFORM_WORKCARD;
$xform = PhabricatorFileTransform::getTransformByKey($xform_key)
->executeTransform($file);
$object->setProperty('cover.filePHID', $file->getPHID());
$object->setProperty('cover.thumbnailPHID', $xform->getPHID());
}
public function getTitle() {
$old = $this->getOldValue();
$new = $this->getNewValue();
if ($old === null) {
return pht(
'%s set the cover image to %s.',
$this->renderAuthor(),
$this->renderHandle($new));
}
return pht(
'%s updated the cover image to %s.',
$this->renderAuthor(),
$this->renderHandle($new));
}
public function getTitleForFeed() {
$old = $this->getOldValue();
if ($old === null) {
return pht(
'%s added a cover image to %s.',
$this->renderAuthor(),
$this->renderObject());
}
return pht(
'%s updated the cover image for %s.',
$this->renderAuthor(),
$this->renderObject());
}
public function validateTransactions($object, array $xactions) {
$errors = array();
$viewer = $this->getActor();
foreach ($xactions as $xaction) {
$file_phid = $xaction->getNewValue();
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($file_phid))
->executeOne();
if (!$file) {
$errors[] = $this->newInvalidError(
- pht('"%s" is not a valid file PHID.',
- $file_phid));
- } else {
- if (!$file->isViewableImage()) {
- $mime_type = $file->getMimeType();
- $errors[] = $this->newInvalidError(
- pht('File mime type of "%s" is not a valid viewable image.',
- $mime_type));
- }
+ pht(
+ 'File PHID ("%s") is invalid, or you do not have permission '.
+ 'to view it.',
+ $file_phid),
+ $xaction);
+ continue;
}
+ if (!$file->isViewableImage()) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'File ("%s", with MIME type "%s") is not a viewable image file.',
+ $file_phid,
+ $file->getMimeType()),
+ $xaction);
+ continue;
+ }
+
+ if (!$file->isTransformableImage()) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'File ("%s", with MIME type "%s") can not be transformed into '.
+ 'a thumbnail. You may be missing support for this file type in '.
+ 'the "GD" extension.',
+ $file_phid,
+ $file->getMimeType()),
+ $xaction);
+ continue;
+ }
}
return $errors;
}
public function getIcon() {
return 'fa-image';
}
}
diff --git a/src/applications/maniphest/xaction/ManiphestTaskSubpriorityTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskSubpriorityTransaction.php
index 49d227b7f..c88ee8aa0 100644
--- a/src/applications/maniphest/xaction/ManiphestTaskSubpriorityTransaction.php
+++ b/src/applications/maniphest/xaction/ManiphestTaskSubpriorityTransaction.php
@@ -1,21 +1,22 @@
<?php
final class ManiphestTaskSubpriorityTransaction
extends ManiphestTaskTransactionType {
const TRANSACTIONTYPE = 'subpriority';
public function generateOldValue($object) {
- return $object->getSubpriority();
+ return null;
}
public function applyInternalEffects($object, $value) {
- $object->setSubpriority($value);
+ // This transaction is obsolete, but we're keeping the class around so it
+ // is hidden from timelines until we destroy the actual transaction data.
+ throw new PhutilMethodNotImplementedException();
}
public function shouldHide() {
return true;
}
-
}
diff --git a/src/applications/maniphest/xaction/ManiphestTaskTitleTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskTitleTransaction.php
index e4ec2a132..7dd921776 100644
--- a/src/applications/maniphest/xaction/ManiphestTaskTitleTransaction.php
+++ b/src/applications/maniphest/xaction/ManiphestTaskTitleTransaction.php
@@ -1,86 +1,104 @@
<?php
final class ManiphestTaskTitleTransaction
extends ManiphestTaskTransactionType {
const TRANSACTIONTYPE = 'title';
public function generateOldValue($object) {
return $object->getTitle();
}
public function applyInternalEffects($object, $value) {
$object->setTitle($value);
}
public function getActionStrength() {
return 1.4;
}
public function getActionName() {
$old = $this->getOldValue();
if (!strlen($old)) {
return pht('Created');
}
return pht('Retitled');
}
public function getTitle() {
$old = $this->getOldValue();
if (!strlen($old)) {
return pht(
'%s created this task.',
$this->renderAuthor());
}
return pht(
'%s renamed this task from %s to %s.',
$this->renderAuthor(),
$this->renderOldValue(),
$this->renderNewValue());
}
public function getTitleForFeed() {
$old = $this->getOldValue();
if ($old === null) {
return pht(
'%s created %s.',
$this->renderAuthor(),
$this->renderObject());
}
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->getTitle(), $xactions)) {
- $errors[] = $this->newRequiredError(
- pht('Tasks must have a title.'));
+ // If the user is acting via "Bulk Edit" or another workflow which
+ // continues on missing fields, they may be applying a transaction which
+ // removes the task title. Mark these transactions as invalid first,
+ // then flag the missing field error if we don't find any more specific
+ // problems.
+
+ foreach ($xactions as $xaction) {
+ $new = $xaction->getNewValue();
+ if (!strlen($new)) {
+ $errors[] = $this->newInvalidError(
+ pht('Tasks must have a title.'),
+ $xaction);
+ continue;
+ }
+ }
+
+ if (!$errors) {
+ if ($this->isEmptyTextTransaction($object->getTitle(), $xactions)) {
+ $errors[] = $this->newRequiredError(
+ pht('Tasks must have a title.'));
+ }
}
return $errors;
}
public function getTransactionTypeForConduit($xaction) {
return 'title';
}
public function getFieldValuesForConduit($xaction, $data) {
return array(
'old' => $xaction->getOldValue(),
'new' => $xaction->getNewValue(),
);
}
}
diff --git a/src/applications/maniphest/xaction/ManiphestTaskUnblockTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskUnblockTransaction.php
index 8833e62b7..cb6c80604 100644
--- a/src/applications/maniphest/xaction/ManiphestTaskUnblockTransaction.php
+++ b/src/applications/maniphest/xaction/ManiphestTaskUnblockTransaction.php
@@ -1,126 +1,136 @@
<?php
final class ManiphestTaskUnblockTransaction
extends ManiphestTaskTransactionType {
const TRANSACTIONTYPE = 'unblock';
public function generateOldValue($object) {
return null;
}
public function getActionName() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$old_status = head($old);
$new_status = head($new);
$old_closed = ManiphestTaskStatus::isClosedStatus($old_status);
$new_closed = ManiphestTaskStatus::isClosedStatus($new_status);
if ($old_closed && !$new_closed) {
return pht('Block');
} else if (!$old_closed && $new_closed) {
return pht('Unblock');
} else {
return pht('Blocker');
}
}
public function getTitle() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$blocker_phid = key($new);
$old_status = head($old);
$new_status = head($new);
$old_closed = ManiphestTaskStatus::isClosedStatus($old_status);
$new_closed = ManiphestTaskStatus::isClosedStatus($new_status);
$old_name = ManiphestTaskStatus::getTaskStatusName($old_status);
$new_name = ManiphestTaskStatus::getTaskStatusName($new_status);
if ($this->getMetadataValue('blocker.new')) {
return pht(
'%s created subtask %s.',
$this->renderAuthor(),
$this->renderHandle($blocker_phid));
} else if ($old_closed && !$new_closed) {
return pht(
'%s reopened subtask %s as %s.',
$this->renderAuthor(),
$this->renderHandle($blocker_phid),
$this->renderValue($new_name));
} else if (!$old_closed && $new_closed) {
return pht(
'%s closed subtask %s as %s.',
$this->renderAuthor(),
$this->renderHandle($blocker_phid),
$this->renderValue($new_name));
} else {
return pht(
'%s changed the status of subtask %s from %s to %s.',
$this->renderAuthor(),
$this->renderHandle($blocker_phid),
$this->renderValue($old_name),
$this->renderValue($new_name));
}
}
public function getTitleForFeed() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$blocker_phid = key($new);
$old_status = head($old);
$new_status = head($new);
$old_closed = ManiphestTaskStatus::isClosedStatus($old_status);
$new_closed = ManiphestTaskStatus::isClosedStatus($new_status);
$old_name = ManiphestTaskStatus::getTaskStatusName($old_status);
$new_name = ManiphestTaskStatus::getTaskStatusName($new_status);
if ($old_closed && !$new_closed) {
return pht(
'%s reopened %s, a subtask of %s, as %s.',
$this->renderAuthor(),
$this->renderHandle($blocker_phid),
$this->renderObject(),
$this->renderValue($new_name));
} else if (!$old_closed && $new_closed) {
return pht(
'%s closed %s, a subtask of %s, as %s.',
$this->renderAuthor(),
$this->renderHandle($blocker_phid),
$this->renderObject(),
$this->renderValue($new_name));
} else {
return pht(
'%s changed the status of %s, a subtask of %s, '.
'from %s to %s.',
$this->renderAuthor(),
$this->renderHandle($blocker_phid),
$this->renderObject(),
$this->renderValue($old_name),
$this->renderValue($new_name));
}
}
public function getIcon() {
return 'fa-shield';
}
public function shouldHideForFeed() {
// Hide "alice created X, a task blocking Y." from feed because it
// will almost always appear adjacent to "alice created Y".
$is_new = $this->getMetadataValue('blocker.new');
if ($is_new) {
return true;
}
return parent::shouldHideForFeed();
}
+ public function getRequiredCapabilities(
+ $object,
+ PhabricatorApplicationTransaction $xaction) {
+
+ // When you close a task, we want to apply this transaction to its parents
+ // even if you can not edit (or even see) those parents, so don't require
+ // any capabilities. See PHI1059.
+
+ return null;
+ }
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailAdapter.php b/src/applications/metamta/adapter/PhabricatorMailAdapter.php
index 4fb262626..8c1a6c0ba 100644
--- a/src/applications/metamta/adapter/PhabricatorMailAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailAdapter.php
@@ -1,140 +1,173 @@
<?php
abstract class PhabricatorMailAdapter
extends Phobject {
private $key;
private $priority;
private $media;
private $options = array();
private $supportsInbound = true;
private $supportsOutbound = true;
private $mediaMap;
final public function getAdapterType() {
return $this->getPhobjectClassConstant('ADAPTERTYPE');
}
final public static function getAllAdapters() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getAdapterType')
->execute();
}
abstract public function getSupportedMessageTypes();
abstract public function sendMessage(PhabricatorMailExternalMessage $message);
/**
* Return true if this adapter supports setting a "Message-ID" when sending
* email.
*
* This is an ugly implementation detail because mail threading is a horrible
* mess, implemented differently by every client in existence.
*/
public function supportsMessageIDHeader() {
return false;
}
final public function supportsMessageType($message_type) {
if ($this->mediaMap === null) {
$media_map = $this->getSupportedMessageTypes();
$media_map = array_fuse($media_map);
if ($this->media) {
$config_map = $this->media;
$config_map = array_fuse($config_map);
$media_map = array_intersect_key($media_map, $config_map);
}
$this->mediaMap = $media_map;
}
return isset($this->mediaMap[$message_type]);
}
final public function setMedia(array $media) {
$native_map = $this->getSupportedMessageTypes();
$native_map = array_fuse($native_map);
foreach ($media as $medium) {
if (!isset($native_map[$medium])) {
throw new Exception(
pht(
'Adapter ("%s") is configured for medium "%s", but this is not '.
'a supported delivery medium. Supported media are: %s.',
$medium,
implode(', ', $native_map)));
}
}
$this->media = $media;
$this->mediaMap = null;
return $this;
}
final public function getMedia() {
return $this->media;
}
final public function setKey($key) {
$this->key = $key;
return $this;
}
final public function getKey() {
return $this->key;
}
final public function setPriority($priority) {
$this->priority = $priority;
return $this;
}
final public function getPriority() {
return $this->priority;
}
final public function setSupportsInbound($supports_inbound) {
$this->supportsInbound = $supports_inbound;
return $this;
}
final public function getSupportsInbound() {
return $this->supportsInbound;
}
final public function setSupportsOutbound($supports_outbound) {
$this->supportsOutbound = $supports_outbound;
return $this;
}
final public function getSupportsOutbound() {
return $this->supportsOutbound;
}
final public function getOption($key) {
if (!array_key_exists($key, $this->options)) {
throw new Exception(
pht(
'Mailer ("%s") is attempting to access unknown option ("%s").',
get_class($this),
$key));
}
return $this->options[$key];
}
final public function setOptions(array $options) {
$this->validateOptions($options);
$this->options = $options;
return $this;
}
abstract protected function validateOptions(array $options);
abstract public function newDefaultOptions();
+ final protected function guessIfHostSupportsMessageID($config, $host) {
+ // See T13265. Mailers like "SMTP" and "sendmail" usually allow us to
+ // set the "Message-ID" header to a value we choose, but we may not be
+ // able to if the mailer is being used as API glue and the outbound
+ // pathway ends up routing to a service with an SMTP API that selects
+ // its own "Message-ID" header, like Amazon SES.
+
+ // If users configured a behavior explicitly, use that behavior.
+ if ($config !== null) {
+ return $config;
+ }
+
+ // If the server we're connecting to is part of a service that we know
+ // does not support "Message-ID", guess that we don't support "Message-ID".
+ if ($host !== null) {
+ $host_blocklist = array(
+ '/\.amazonaws\.com\z/',
+ '/\.postmarkapp\.com\z/',
+ '/\.sendgrid\.net\z/',
+ );
+
+ $host = phutil_utf8_strtolower($host);
+ foreach ($host_blocklist as $regexp) {
+ if (preg_match($regexp, $host)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php b/src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php
index a289e5bc7..793cd5609 100644
--- a/src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php
@@ -1,71 +1,67 @@
<?php
final class PhabricatorMailAmazonSESAdapter
extends PhabricatorMailAdapter {
const ADAPTERTYPE = 'ses';
public function getSupportedMessageTypes() {
return array(
PhabricatorMailEmailMessage::MESSAGETYPE,
);
}
- public function supportsMessageIDHeader() {
- return false;
- }
-
protected function validateOptions(array $options) {
PhutilTypeSpec::checkMap(
$options,
array(
'access-key' => 'string',
'secret-key' => 'string',
'endpoint' => 'string',
));
}
public function newDefaultOptions() {
return array(
'access-key' => null,
'secret-key' => null,
'endpoint' => null,
);
}
/**
* @phutil-external-symbol class PHPMailerLite
*/
public function sendMessage(PhabricatorMailExternalMessage $message) {
$root = phutil_get_library_root('phabricator');
$root = dirname($root);
require_once $root.'/externals/phpmailer/class.phpmailer-lite.php';
$mailer = PHPMailerLite::newFromMessage($message);
$mailer->Mailer = 'amazon-ses';
$mailer->customMailer = $this;
$mailer->Send();
}
/**
* @phutil-external-symbol class SimpleEmailService
*/
public function executeSend($body) {
$key = $this->getOption('access-key');
$secret = $this->getOption('secret-key');
$endpoint = $this->getOption('endpoint');
$root = phutil_get_library_root('phabricator');
$root = dirname($root);
require_once $root.'/externals/amazon-ses/ses.php';
$service = new SimpleEmailService($key, $secret, $endpoint);
$service->enableUseExceptions(true);
return $service->sendRawEmail($body);
}
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailMailgunAdapter.php b/src/applications/metamta/adapter/PhabricatorMailMailgunAdapter.php
index 9eb478efc..8223ee810 100644
--- a/src/applications/metamta/adapter/PhabricatorMailMailgunAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailMailgunAdapter.php
@@ -1,132 +1,136 @@
<?php
/**
* Mail adapter that uses Mailgun's web API to deliver email.
*/
final class PhabricatorMailMailgunAdapter
extends PhabricatorMailAdapter {
const ADAPTERTYPE = 'mailgun';
public function getSupportedMessageTypes() {
return array(
PhabricatorMailEmailMessage::MESSAGETYPE,
);
}
public function supportsMessageIDHeader() {
return true;
}
protected function validateOptions(array $options) {
PhutilTypeSpec::checkMap(
$options,
array(
'api-key' => 'string',
'domain' => 'string',
+ 'api-hostname' => 'string',
));
}
public function newDefaultOptions() {
return array(
'api-key' => null,
'domain' => null,
+ 'api-hostname' => 'api.mailgun.net',
);
}
public function sendMessage(PhabricatorMailExternalMessage $message) {
$api_key = $this->getOption('api-key');
$domain = $this->getOption('domain');
+ $api_hostname = $this->getOption('api-hostname');
$params = array();
$subject = $message->getSubject();
if ($subject !== null) {
$params['subject'] = $subject;
}
$from_address = $message->getFromAddress();
if ($from_address) {
$params['from'] = (string)$from_address;
}
$to_addresses = $message->getToAddresses();
if ($to_addresses) {
$to = array();
foreach ($to_addresses as $address) {
$to[] = (string)$address;
}
$params['to'] = implode(', ', $to);
}
$cc_addresses = $message->getCCAddresses();
if ($cc_addresses) {
$cc = array();
foreach ($cc_addresses as $address) {
$cc[] = (string)$address;
}
$params['cc'] = implode(', ', $cc);
}
$reply_address = $message->getReplyToAddress();
if ($reply_address) {
$params['h:reply-to'] = (string)$reply_address;
}
$headers = $message->getHeaders();
if ($headers) {
foreach ($headers as $header) {
$name = $header->getName();
$value = $header->getValue();
$params['h:'.$name] = $value;
}
}
$text_body = $message->getTextBody();
if ($text_body !== null) {
$params['text'] = $text_body;
}
$html_body = $message->getHTMLBody();
if ($html_body !== null) {
$params['html'] = $html_body;
}
$mailgun_uri = urisprintf(
- 'https://api.mailgun.net/v2/%s/messages',
+ 'https://%s/v2/%s/messages',
+ $api_hostname,
$domain);
$future = id(new HTTPSFuture($mailgun_uri, $params))
->setMethod('POST')
->setHTTPBasicAuthCredentials('api', new PhutilOpaqueEnvelope($api_key))
->setTimeout(60);
$attachments = $message->getAttachments();
foreach ($attachments as $attachment) {
$future->attachFileData(
'attachment',
$attachment->getData(),
$attachment->getFilename(),
$attachment->getMimeType());
}
list($body) = $future->resolvex();
$response = null;
try {
$response = phutil_json_decode($body);
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht('Failed to JSON decode response.'),
$ex);
}
if (!idx($response, 'id')) {
$message = $response['message'];
throw new Exception(
pht(
'Request failed with errors: %s.',
$message));
}
}
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php b/src/applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php
index d84d8f8bf..2381ff04b 100644
--- a/src/applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php
@@ -1,128 +1,124 @@
<?php
final class PhabricatorMailPostmarkAdapter
extends PhabricatorMailAdapter {
const ADAPTERTYPE = 'postmark';
public function getSupportedMessageTypes() {
return array(
PhabricatorMailEmailMessage::MESSAGETYPE,
);
}
- public function supportsMessageIDHeader() {
- return true;
- }
-
protected function validateOptions(array $options) {
PhutilTypeSpec::checkMap(
$options,
array(
'access-token' => 'string',
'inbound-addresses' => 'list<string>',
));
// Make sure this is properly formatted.
PhutilCIDRList::newList($options['inbound-addresses']);
}
public function newDefaultOptions() {
return array(
'access-token' => null,
'inbound-addresses' => array(
// Via Postmark support circa February 2018, see:
//
// https://postmarkapp.com/support/article/800-ips-for-firewalls
//
// "Configuring Outbound Email" should be updated if this changes.
//
// These addresses were last updated in January 2019.
'50.31.156.6/32',
'50.31.156.77/32',
'18.217.206.57/32',
),
);
}
public function sendMessage(PhabricatorMailExternalMessage $message) {
$access_token = $this->getOption('access-token');
$parameters = array();
$subject = $message->getSubject();
if ($subject !== null) {
$parameters['Subject'] = $subject;
}
$from_address = $message->getFromAddress();
if ($from_address) {
$parameters['From'] = (string)$from_address;
}
$to_addresses = $message->getToAddresses();
if ($to_addresses) {
$to = array();
foreach ($to_addresses as $address) {
$to[] = (string)$address;
}
$parameters['To'] = implode(', ', $to);
}
$cc_addresses = $message->getCCAddresses();
if ($cc_addresses) {
$cc = array();
foreach ($cc_addresses as $address) {
$cc[] = (string)$address;
}
$parameters['Cc'] = implode(', ', $cc);
}
$reply_address = $message->getReplyToAddress();
if ($reply_address) {
$parameters['ReplyTo'] = (string)$reply_address;
}
$headers = $message->getHeaders();
if ($headers) {
$list = array();
foreach ($headers as $header) {
$list[] = array(
'Name' => $header->getName(),
'Value' => $header->getValue(),
);
}
$parameters['Headers'] = $list;
}
$text_body = $message->getTextBody();
if ($text_body !== null) {
$parameters['TextBody'] = $text_body;
}
$html_body = $message->getHTMLBody();
if ($html_body !== null) {
$parameters['HtmlBody'] = $html_body;
}
$attachments = $message->getAttachments();
if ($attachments) {
$files = array();
foreach ($attachments as $attachment) {
$files[] = array(
'Name' => $attachment->getFilename(),
'ContentType' => $attachment->getMimeType(),
'Content' => base64_encode($attachment->getData()),
);
}
$parameters['Attachments'] = $files;
}
id(new PhutilPostmarkFuture())
->setAccessToken($access_token)
->setMethod('email', $parameters)
->setTimeout(60)
->resolve();
}
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailSMTPAdapter.php b/src/applications/metamta/adapter/PhabricatorMailSMTPAdapter.php
index a3c629827..abbda4014 100644
--- a/src/applications/metamta/adapter/PhabricatorMailSMTPAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailSMTPAdapter.php
@@ -1,154 +1,158 @@
<?php
final class PhabricatorMailSMTPAdapter
extends PhabricatorMailAdapter {
const ADAPTERTYPE = 'smtp';
public function getSupportedMessageTypes() {
return array(
PhabricatorMailEmailMessage::MESSAGETYPE,
);
}
public function supportsMessageIDHeader() {
- return true;
+ return $this->guessIfHostSupportsMessageID(
+ $this->getOption('message-id'),
+ $this->getOption('host'));
}
protected function validateOptions(array $options) {
PhutilTypeSpec::checkMap(
$options,
array(
'host' => 'string|null',
'port' => 'int',
'user' => 'string|null',
'password' => 'string|null',
'protocol' => 'string|null',
+ 'message-id' => 'bool|null',
));
}
public function newDefaultOptions() {
return array(
'host' => null,
'port' => 25,
'user' => null,
'password' => null,
'protocol' => null,
+ 'message-id' => null,
);
}
/**
* @phutil-external-symbol class PHPMailer
*/
public function sendMessage(PhabricatorMailExternalMessage $message) {
$root = phutil_get_library_root('phabricator');
$root = dirname($root);
require_once $root.'/externals/phpmailer/class.phpmailer.php';
$smtp = new PHPMailer($use_exceptions = true);
$smtp->CharSet = 'utf-8';
$smtp->Encoding = 'base64';
// By default, PHPMailer sends one mail per recipient. We handle
// combining or separating To and Cc higher in the stack, so tell it to
// send mail exactly like we ask.
$smtp->SingleTo = false;
$smtp->IsSMTP();
$smtp->Host = $this->getOption('host');
$smtp->Port = $this->getOption('port');
$user = $this->getOption('user');
if (strlen($user)) {
$smtp->SMTPAuth = true;
$smtp->Username = $user;
$smtp->Password = $this->getOption('password');
}
$protocol = $this->getOption('protocol');
if ($protocol) {
$protocol = phutil_utf8_strtolower($protocol);
$smtp->SMTPSecure = $protocol;
}
$subject = $message->getSubject();
if ($subject !== null) {
$smtp->Subject = $subject;
}
$from_address = $message->getFromAddress();
if ($from_address) {
$smtp->SetFrom(
$from_address->getAddress(),
(string)$from_address->getDisplayName(),
$crazy_side_effects = false);
}
$reply_address = $message->getReplyToAddress();
if ($reply_address) {
$smtp->AddReplyTo(
$reply_address->getAddress(),
(string)$reply_address->getDisplayName());
}
$to_addresses = $message->getToAddresses();
if ($to_addresses) {
foreach ($to_addresses as $address) {
$smtp->AddAddress(
$address->getAddress(),
(string)$address->getDisplayName());
}
}
$cc_addresses = $message->getCCAddresses();
if ($cc_addresses) {
foreach ($cc_addresses as $address) {
$smtp->AddCC(
$address->getAddress(),
(string)$address->getDisplayName());
}
}
$headers = $message->getHeaders();
if ($headers) {
$list = array();
foreach ($headers as $header) {
$name = $header->getName();
$value = $header->getValue();
if (phutil_utf8_strtolower($name) === 'message-id') {
$smtp->MessageID = $value;
} else {
$smtp->AddCustomHeader("{$name}: {$value}");
}
}
}
$text_body = $message->getTextBody();
if ($text_body !== null) {
$smtp->Body = $text_body;
}
$html_body = $message->getHTMLBody();
if ($html_body !== null) {
$smtp->IsHTML(true);
$smtp->Body = $html_body;
if ($text_body !== null) {
$smtp->AltBody = $text_body;
}
}
$attachments = $message->getAttachments();
if ($attachments) {
foreach ($attachments as $attachment) {
$smtp->AddStringAttachment(
$attachment->getData(),
$attachment->getFilename(),
'base64',
$attachment->getMimeType());
}
}
$smtp->Send();
}
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailSendmailAdapter.php b/src/applications/metamta/adapter/PhabricatorMailSendmailAdapter.php
index 05f3c909a..a60c0e5a4 100644
--- a/src/applications/metamta/adapter/PhabricatorMailSendmailAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailSendmailAdapter.php
@@ -1,45 +1,46 @@
<?php
final class PhabricatorMailSendmailAdapter
extends PhabricatorMailAdapter {
const ADAPTERTYPE = 'sendmail';
-
public function getSupportedMessageTypes() {
return array(
PhabricatorMailEmailMessage::MESSAGETYPE,
);
}
public function supportsMessageIDHeader() {
- return true;
+ return $this->guessIfHostSupportsMessageID(
+ $this->getOption('message-id'),
+ null);
}
protected function validateOptions(array $options) {
PhutilTypeSpec::checkMap(
$options,
array(
- 'encoding' => 'string',
+ 'message-id' => 'bool|null',
));
}
public function newDefaultOptions() {
return array(
- 'encoding' => 'base64',
+ 'message-id' => null,
);
}
/**
* @phutil-external-symbol class PHPMailerLite
*/
public function sendMessage(PhabricatorMailExternalMessage $message) {
$root = phutil_get_library_root('phabricator');
$root = dirname($root);
require_once $root.'/externals/phpmailer/class.phpmailer-lite.php';
$mailer = PHPMailerLite::newFromMessage($message);
$mailer->Send();
}
}
diff --git a/src/applications/metamta/adapter/__tests__/PhabricatorMailAdapterTestCase.php b/src/applications/metamta/adapter/__tests__/PhabricatorMailAdapterTestCase.php
new file mode 100644
index 000000000..9c194f24c
--- /dev/null
+++ b/src/applications/metamta/adapter/__tests__/PhabricatorMailAdapterTestCase.php
@@ -0,0 +1,96 @@
+<?php
+
+final class PhabricatorMailAdapterTestCase
+ extends PhabricatorTestCase {
+
+ public function testSupportsMessageID() {
+ $cases = array(
+ array(
+ pht('Amazon SES'),
+ false,
+ new PhabricatorMailAmazonSESAdapter(),
+ array(
+ 'access-key' => 'test',
+ 'secret-key' => 'test',
+ 'endpoint' => 'test',
+ ),
+ ),
+
+ array(
+ pht('Mailgun'),
+ true,
+ new PhabricatorMailMailgunAdapter(),
+ array(
+ 'api-key' => 'test',
+ 'domain' => 'test',
+ 'api-hostname' => 'test',
+ ),
+ ),
+
+ array(
+ pht('Sendmail'),
+ true,
+ new PhabricatorMailSendmailAdapter(),
+ array(),
+ ),
+
+ array(
+ pht('Sendmail (Explicit Config)'),
+ false,
+ new PhabricatorMailSendmailAdapter(),
+ array(
+ 'message-id' => false,
+ ),
+ ),
+
+ array(
+ pht('SMTP (Local)'),
+ true,
+ new PhabricatorMailSMTPAdapter(),
+ array(),
+ ),
+
+ array(
+ pht('SMTP (Local + Explicit)'),
+ false,
+ new PhabricatorMailSMTPAdapter(),
+ array(
+ 'message-id' => false,
+ ),
+ ),
+
+ array(
+ pht('SMTP (AWS)'),
+ false,
+ new PhabricatorMailSMTPAdapter(),
+ array(
+ 'host' => 'test.amazonaws.com',
+ ),
+ ),
+
+ array(
+ pht('SMTP (AWS + Explicit)'),
+ true,
+ new PhabricatorMailSMTPAdapter(),
+ array(
+ 'host' => 'test.amazonaws.com',
+ 'message-id' => true,
+ ),
+ ),
+
+ );
+
+ foreach ($cases as $case) {
+ list($label, $expect, $mailer, $options) = $case;
+
+ $defaults = $mailer->newDefaultOptions();
+ $mailer->setOptions($options + $defaults);
+
+ $actual = $mailer->supportsMessageIDHeader();
+
+ $this->assertEqual($expect, $actual, pht('Message-ID: %s', $label));
+ }
+ }
+
+
+}
diff --git a/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php b/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php
index c13835e6f..2f9ddcf22 100644
--- a/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php
+++ b/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php
@@ -1,423 +1,422 @@
<?php
final class PhabricatorMetaMTAApplicationEmailPanel
extends PhabricatorApplicationConfigurationPanel {
public function getPanelKey() {
return 'email';
}
public function shouldShowForApplication(
PhabricatorApplication $application) {
return $application->supportsEmailIntegration();
}
public function buildConfigurationPagePanel() {
$viewer = $this->getViewer();
$application = $this->getApplication();
$table = $this->buildEmailTable($is_edit = false, null);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$application,
PhabricatorPolicyCapability::CAN_EDIT);
$header = id(new PHUIHeaderView())
->setHeader(pht('Application Emails'))
->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setText(pht('Edit Application Emails'))
->setIcon('fa-pencil')
->setHref($this->getPanelURI())
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($table);
return $box;
}
public function handlePanelRequest(
AphrontRequest $request,
PhabricatorController $controller) {
$viewer = $request->getViewer();
$application = $this->getApplication();
$path = $request->getURIData('path');
if (strlen($path)) {
return new Aphront404Response();
}
- $uri = $request->getRequestURI();
- $uri->setQueryParams(array());
+ $uri = new PhutilURI($request->getPath());
$new = $request->getStr('new');
$edit = $request->getInt('edit');
$delete = $request->getInt('delete');
if ($new) {
return $this->returnNewAddressResponse($request, $uri, $application);
}
if ($edit) {
return $this->returnEditAddressResponse($request, $uri, $edit);
}
if ($delete) {
return $this->returnDeleteAddressResponse($request, $uri, $delete);
}
$table = $this->buildEmailTable(
$is_edit = true,
$request->getInt('id'));
$form = id(new AphrontFormView())
->setUser($viewer);
$crumbs = $controller->buildPanelCrumbs($this);
$crumbs->addTextCrumb(pht('Edit Application Emails'));
$crumbs->setBorder(true);
$header = id(new PHUIHeaderView())
->setHeader(pht('Edit Application Emails: %s', $application->getName()))
->setSubheader($application->getAppEmailBlurb())
->setHeaderIcon('fa-pencil');
$icon = id(new PHUIIconView())
->setIcon('fa-plus');
$button = new PHUIButtonView();
$button->setText(pht('Add New Address'));
$button->setTag('a');
$button->setHref($uri->alter('new', 'true'));
$button->setIcon($icon);
$button->addSigil('workflow');
$header->addActionLink($button);
$object_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Emails'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($table);
$title = $application->getName();
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter($object_box);
return $controller->buildPanelPage(
$this,
$title,
$crumbs,
$view);
}
private function returnNewAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
PhabricatorApplication $application) {
$viewer = $request->getUser();
$email_object =
PhabricatorMetaMTAApplicationEmail::initializeNewAppEmail($viewer)
->setApplicationPHID($application->getPHID());
return $this->returnSaveAddressResponse(
$request,
$uri,
$email_object,
$is_new = true);
}
private function returnEditAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_object_id) {
$viewer = $request->getUser();
$email_object = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer($viewer)
->withIDs(array($email_object_id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$email_object) {
return new Aphront404Response();
}
return $this->returnSaveAddressResponse(
$request,
$uri,
$email_object,
$is_new = false);
}
private function returnSaveAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
PhabricatorMetaMTAApplicationEmail $email_object,
$is_new) {
$viewer = $request->getUser();
$config_default =
PhabricatorMetaMTAApplicationEmail::CONFIG_DEFAULT_AUTHOR;
$e_email = true;
$v_email = $email_object->getAddress();
$e_space = null;
$v_space = $email_object->getSpacePHID();
$v_default = $email_object->getConfigValue($config_default);
$validation_exception = null;
if ($request->isDialogFormPost()) {
$e_email = null;
$v_email = trim($request->getStr('email'));
$v_space = $request->getStr('spacePHID');
$v_default = $request->getArr($config_default);
$v_default = nonempty(head($v_default), null);
$type_address =
PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS;
$type_space = PhabricatorTransactions::TYPE_SPACE;
$type_config =
PhabricatorMetaMTAApplicationEmailTransaction::TYPE_CONFIG;
$key_config = PhabricatorMetaMTAApplicationEmailTransaction::KEY_CONFIG;
$xactions = array();
$xactions[] = id(new PhabricatorMetaMTAApplicationEmailTransaction())
->setTransactionType($type_address)
->setNewValue($v_email);
$xactions[] = id(new PhabricatorMetaMTAApplicationEmailTransaction())
->setTransactionType($type_space)
->setNewValue($v_space);
$xactions[] = id(new PhabricatorMetaMTAApplicationEmailTransaction())
->setTransactionType($type_config)
->setMetadataValue($key_config, $config_default)
->setNewValue($v_default);
$editor = id(new PhabricatorMetaMTAApplicationEmailEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true);
try {
$editor->applyTransactions($email_object, $xactions);
return id(new AphrontRedirectResponse())->setURI(
$uri->alter('highlight', $email_object->getID()));
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
$e_email = $ex->getShortMessage($type_address);
$e_space = $ex->getShortMessage($type_space);
}
}
if ($v_default) {
$v_default = array($v_default);
} else {
$v_default = array();
}
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Email'))
->setName('email')
->setValue($v_email)
->setError($e_email));
if (PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer)) {
$form->appendControl(
id(new AphrontFormSelectControl())
->setLabel(pht('Space'))
->setName('spacePHID')
->setValue($v_space)
->setError($e_space)
->setOptions(
PhabricatorSpacesNamespaceQuery::getSpaceOptionsForViewer(
$viewer,
$v_space)));
}
$form
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorPeopleDatasource())
->setLabel(pht('Default Author'))
->setName($config_default)
->setLimit(1)
->setValue($v_default)
->setCaption(
pht(
'Used if the "From:" address does not map to a user account. '.
'Setting a default author will allow anyone on the public '.
'internet to create objects in Phabricator by sending email to '.
'this address.')));
if ($is_new) {
$title = pht('New Address');
} else {
$title = pht('Edit Address');
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setWidth(AphrontDialogView::WIDTH_FORM)
->setTitle($title)
->setValidationException($validation_exception)
->appendForm($form)
->addSubmitButton(pht('Save'))
->addCancelButton($uri);
if ($is_new) {
$dialog->addHiddenInput('new', 'true');
}
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function returnDeleteAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_object_id) {
$viewer = $this->getViewer();
$email_object = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer($viewer)
->withIDs(array($email_object_id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$email_object) {
return new Aphront404Response();
}
if ($request->isDialogFormPost()) {
$engine = new PhabricatorDestructionEngine();
$engine->destroyObject($email_object);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addHiddenInput('delete', $email_object_id)
->setTitle(pht('Delete Address'))
->appendParagraph(pht(
'Are you sure you want to delete this email address?'))
->addSubmitButton(pht('Delete'))
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function buildEmailTable($is_edit, $highlight) {
$viewer = $this->getViewer();
$application = $this->getApplication();
$uri = new PhutilURI($this->getPanelURI());
$emails = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer($viewer)
->withApplicationPHIDs(array($application->getPHID()))
->execute();
$rowc = array();
$rows = array();
foreach ($emails as $email) {
$button_edit = javelin_tag(
'a',
array(
'class' => 'button small button-grey',
'href' => $uri->alter('edit', $email->getID()),
'sigil' => 'workflow',
),
pht('Edit'));
$button_remove = javelin_tag(
'a',
array(
'class' => 'button small button-grey',
'href' => $uri->alter('delete', $email->getID()),
'sigil' => 'workflow',
),
pht('Delete'));
if ($highlight == $email->getID()) {
$rowc[] = 'highlighted';
} else {
$rowc[] = null;
}
$space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID($email);
if ($space_phid) {
$email_space = $viewer->renderHandle($space_phid);
} else {
$email_space = null;
}
$default_author_phid = $email->getDefaultAuthorPHID();
if (!$default_author_phid) {
$default_author = phutil_tag('em', array(), pht('None'));
} else {
$default_author = $viewer->renderHandle($default_author_phid);
}
$rows[] = array(
$email_space,
$email->getAddress(),
$default_author,
$button_edit,
$button_remove,
);
}
$table = id(new AphrontTableView($rows))
->setNoDataString(pht('No application emails created yet.'));
$table->setHeaders(
array(
pht('Space'),
pht('Email'),
pht('Default'),
pht('Edit'),
pht('Delete'),
));
$table->setColumnClasses(
array(
'',
'',
'wide',
'action',
'action',
));
$table->setRowClasses($rowc);
$table->setColumnVisibility(
array(
PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer),
true,
true,
$is_edit,
$is_edit,
));
return $table;
}
}
diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAMailListController.php b/src/applications/metamta/controller/PhabricatorMetaMTAMailListController.php
index 065106855..01d6f1e21 100644
--- a/src/applications/metamta/controller/PhabricatorMetaMTAMailListController.php
+++ b/src/applications/metamta/controller/PhabricatorMetaMTAMailListController.php
@@ -1,30 +1,34 @@
<?php
final class PhabricatorMetaMTAMailListController
extends PhabricatorMetaMTAController {
public function handleRequest(AphrontRequest $request) {
$controller = id(new PhabricatorApplicationSearchController())
->setQueryKey($request->getURIData('queryKey'))
->setSearchEngine(new PhabricatorMetaMTAMailSearchEngine())
->setNavigation($this->buildSideNav());
return $this->delegateToController($controller);
}
public function buildSideNav() {
$user = $this->getRequest()->getUser();
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
id(new PhabricatorMetaMTAMailSearchEngine())
->setViewer($user)
->addNavigationItems($nav->getMenu());
$nav->selectFilter(null);
return $nav;
}
+ public function buildApplicationMenu() {
+ return $this->buildSideNav()->getMenu();
+ }
+
}
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmailTransaction.php b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmailTransaction.php
index 019adb338..af6a6fbb8 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmailTransaction.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmailTransaction.php
@@ -1,23 +1,19 @@
<?php
final class PhabricatorMetaMTAApplicationEmailTransaction
extends PhabricatorApplicationTransaction {
const KEY_CONFIG = 'appemail.config.key';
const TYPE_ADDRESS = 'appemail.address';
const TYPE_CONFIG = 'appemail.config';
public function getApplicationName() {
return 'metamta';
}
public function getApplicationTransactionType() {
return PhabricatorMetaMTAApplicationEmailPHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return null;
- }
-
}
diff --git a/src/applications/multimeter/controller/MultimeterSampleController.php b/src/applications/multimeter/controller/MultimeterSampleController.php
index da09641d2..190a839f6 100644
--- a/src/applications/multimeter/controller/MultimeterSampleController.php
+++ b/src/applications/multimeter/controller/MultimeterSampleController.php
@@ -1,349 +1,350 @@
<?php
final class MultimeterSampleController extends MultimeterController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$group_map = $this->getColumnMap();
$group = explode('.', $request->getStr('group'));
$group = array_intersect($group, array_keys($group_map));
$group = array_fuse($group);
if (empty($group['type'])) {
$group['type'] = 'type';
}
$now = PhabricatorTime::getNow();
$ago = ($now - phutil_units('24 hours in seconds'));
$table = new MultimeterEvent();
$conn = $table->establishConnection('r');
$where = array();
$where[] = qsprintf(
$conn,
'epoch >= %d AND epoch <= %d',
$ago,
$now);
$with = array();
foreach ($group_map as $key => $column) {
// Don't let non-admins filter by viewers, this feels a little too
// invasive of privacy.
if ($key == 'viewer') {
if (!$viewer->getIsAdmin()) {
continue;
}
}
$with[$key] = $request->getStrList($key);
if ($with[$key]) {
$where[] = qsprintf(
$conn,
'%T IN (%Ls)',
$column,
$with[$key]);
}
}
$data = queryfx_all(
$conn,
'SELECT *,
count(*) AS N,
SUM(sampleRate * resourceCost) AS totalCost,
SUM(sampleRate * resourceCost) / SUM(sampleRate) AS averageCost
FROM %T
WHERE %LA
GROUP BY %LC
ORDER BY totalCost DESC, MAX(id) DESC
LIMIT 100',
$table->getTableName(),
$where,
array_select_keys($group_map, $group));
$this->loadDimensions($data);
$phids = array();
foreach ($data as $row) {
$viewer_name = $this->getViewerDimension($row['eventViewerID'])
->getName();
$viewer_phid = $this->getEventViewerPHID($viewer_name);
if ($viewer_phid) {
$phids[] = $viewer_phid;
}
}
$handles = $viewer->loadHandles($phids);
$rows = array();
foreach ($data as $row) {
if ($row['N'] == 1) {
$events_col = $row['id'];
} else {
$events_col = $this->renderGroupingLink(
$group,
'id',
pht('%s Event(s)', new PhutilNumber($row['N'])));
}
if (isset($group['request'])) {
$request_col = $row['requestKey'];
if (!$with['request']) {
$request_col = $this->renderSelectionLink(
'request',
$row['requestKey'],
$request_col);
}
} else {
$request_col = $this->renderGroupingLink($group, 'request');
}
if (isset($group['viewer'])) {
if ($viewer->getIsAdmin()) {
$viewer_col = $this->getViewerDimension($row['eventViewerID'])
->getName();
$viewer_phid = $this->getEventViewerPHID($viewer_col);
if ($viewer_phid) {
$viewer_col = $handles[$viewer_phid]->getName();
}
if (!$with['viewer']) {
$viewer_col = $this->renderSelectionLink(
'viewer',
$row['eventViewerID'],
$viewer_col);
}
} else {
$viewer_col = phutil_tag('em', array(), pht('(Masked)'));
}
} else {
$viewer_col = $this->renderGroupingLink($group, 'viewer');
}
if (isset($group['context'])) {
$context_col = $this->getContextDimension($row['eventContextID'])
->getName();
if (!$with['context']) {
$context_col = $this->renderSelectionLink(
'context',
$row['eventContextID'],
$context_col);
}
} else {
$context_col = $this->renderGroupingLink($group, 'context');
}
if (isset($group['host'])) {
$host_col = $this->getHostDimension($row['eventHostID'])
->getName();
if (!$with['host']) {
$host_col = $this->renderSelectionLink(
'host',
$row['eventHostID'],
$host_col);
}
} else {
$host_col = $this->renderGroupingLink($group, 'host');
}
if (isset($group['label'])) {
$label_col = $this->getLabelDimension($row['eventLabelID'])
->getName();
if (!$with['label']) {
$label_col = $this->renderSelectionLink(
'label',
$row['eventLabelID'],
$label_col);
}
} else {
$label_col = $this->renderGroupingLink($group, 'label');
}
if ($with['type']) {
$type_col = MultimeterEvent::getEventTypeName($row['eventType']);
} else {
$type_col = $this->renderSelectionLink(
'type',
$row['eventType'],
MultimeterEvent::getEventTypeName($row['eventType']));
}
$rows[] = array(
$events_col,
$request_col,
$viewer_col,
$context_col,
$host_col,
$type_col,
$label_col,
MultimeterEvent::formatResourceCost(
$viewer,
$row['eventType'],
$row['averageCost']),
MultimeterEvent::formatResourceCost(
$viewer,
$row['eventType'],
$row['totalCost']),
($row['N'] == 1)
? $row['sampleRate']
: '-',
phabricator_datetime($row['epoch'], $viewer),
);
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('ID'),
pht('Request'),
pht('Viewer'),
pht('Context'),
pht('Host'),
pht('Type'),
pht('Label'),
pht('Avg'),
pht('Cost'),
pht('Rate'),
pht('Epoch'),
))
->setColumnClasses(
array(
null,
null,
null,
null,
null,
null,
'wide',
'n',
'n',
'n',
null,
));
$box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Samples'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($table);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(
pht('Samples'),
$this->getGroupURI(array(), true));
$crumbs->setBorder(true);
$crumb_map = array(
'host' => pht('By Host'),
'context' => pht('By Context'),
'viewer' => pht('By Viewer'),
'request' => pht('By Request'),
'label' => pht('By Label'),
'id' => pht('By ID'),
);
$parts = array();
foreach ($group as $item) {
if ($item == 'type') {
continue;
}
$parts[$item] = $item;
$crumbs->addTextCrumb(
idx($crumb_map, $item, $item),
$this->getGroupURI($parts, true));
}
$header = id(new PHUIHeaderView())
->setHeader(
pht(
'Samples (%s - %s)',
phabricator_datetime($ago, $viewer),
phabricator_datetime($now, $viewer)))
->setHeaderIcon('fa-motorcycle');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter($box);
return $this->newPage()
->setTitle(pht('Samples'))
->setCrumbs($crumbs)
->appendChild($view);
}
private function renderGroupingLink(array $group, $key, $name = null) {
$group[] = $key;
$uri = $this->getGroupURI($group);
if ($name === null) {
$name = pht('(All)');
}
return phutil_tag(
'a',
array(
'href' => $uri,
'style' => 'font-weight: bold',
),
$name);
}
private function getGroupURI(array $group, $wipe = false) {
unset($group['type']);
$uri = clone $this->getRequest()->getRequestURI();
$group = implode('.', $group);
if (!strlen($group)) {
- $group = null;
+ $uri->removeQueryParam('group');
+ } else {
+ $uri->replaceQueryParam('group', $group);
}
- $uri->setQueryParam('group', $group);
if ($wipe) {
foreach ($this->getColumnMap() as $key => $column) {
- $uri->setQueryParam($key, null);
+ $uri->removeQueryParam($key);
}
}
return $uri;
}
private function renderSelectionLink($key, $value, $link_text) {
$value = (array)$value;
$uri = clone $this->getRequest()->getRequestURI();
- $uri->setQueryParam($key, implode(',', $value));
+ $uri->replaceQueryParam($key, implode(',', $value));
return phutil_tag(
'a',
array(
'href' => $uri,
),
$link_text);
}
private function getColumnMap() {
return array(
'type' => 'eventType',
'host' => 'eventHostID',
'context' => 'eventContextID',
'viewer' => 'eventViewerID',
'request' => 'requestKey',
'label' => 'eventLabelID',
'id' => 'id',
);
}
private function getEventViewerPHID($viewer_name) {
if (!strncmp($viewer_name, 'user.', 5)) {
return substr($viewer_name, 5);
}
return null;
}
}
diff --git a/src/applications/notification/client/PhabricatorNotificationServerRef.php b/src/applications/notification/client/PhabricatorNotificationServerRef.php
index b183221ee..46d03a5c3 100644
--- a/src/applications/notification/client/PhabricatorNotificationServerRef.php
+++ b/src/applications/notification/client/PhabricatorNotificationServerRef.php
@@ -1,234 +1,234 @@
<?php
final class PhabricatorNotificationServerRef
extends Phobject {
private $type;
private $host;
private $port;
private $protocol;
private $path;
private $isDisabled;
const KEY_REFS = 'notification.refs';
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
public function setHost($host) {
$this->host = $host;
return $this;
}
public function getHost() {
return $this->host;
}
public function setPort($port) {
$this->port = $port;
return $this;
}
public function getPort() {
return $this->port;
}
public function setProtocol($protocol) {
$this->protocol = $protocol;
return $this;
}
public function getProtocol() {
return $this->protocol;
}
public function setPath($path) {
$this->path = $path;
return $this;
}
public function getPath() {
return $this->path;
}
public function setIsDisabled($is_disabled) {
$this->isDisabled = $is_disabled;
return $this;
}
public function getIsDisabled() {
return $this->isDisabled;
}
public static function getLiveServers() {
$cache = PhabricatorCaches::getRequestCache();
$refs = $cache->getKey(self::KEY_REFS);
if (!$refs) {
$refs = self::newRefs();
$cache->setKey(self::KEY_REFS, $refs);
}
return $refs;
}
public static function newRefs() {
$configs = PhabricatorEnv::getEnvConfig('notification.servers');
$refs = array();
foreach ($configs as $config) {
$ref = id(new self())
->setType($config['type'])
->setHost($config['host'])
->setPort($config['port'])
->setProtocol($config['protocol'])
->setPath(idx($config, 'path'))
->setIsDisabled(idx($config, 'disabled', false));
$refs[] = $ref;
}
return $refs;
}
public static function getEnabledServers() {
$servers = self::getLiveServers();
foreach ($servers as $key => $server) {
if ($server->getIsDisabled()) {
unset($servers[$key]);
}
}
return array_values($servers);
}
public static function getEnabledAdminServers() {
$servers = self::getEnabledServers();
foreach ($servers as $key => $server) {
if (!$server->isAdminServer()) {
unset($servers[$key]);
}
}
return array_values($servers);
}
public static function getEnabledClientServers($with_protocol) {
$servers = self::getEnabledServers();
foreach ($servers as $key => $server) {
if ($server->isAdminServer()) {
unset($servers[$key]);
continue;
}
$protocol = $server->getProtocol();
if ($protocol != $with_protocol) {
unset($servers[$key]);
continue;
}
}
return array_values($servers);
}
public function isAdminServer() {
return ($this->type == 'admin');
}
public function getURI($to_path = null) {
$full_path = rtrim($this->getPath(), '/').'/'.ltrim($to_path, '/');
$uri = id(new PhutilURI('http://'.$this->getHost()))
->setProtocol($this->getProtocol())
->setPort($this->getPort())
->setPath($full_path);
$instance = PhabricatorEnv::getEnvConfig('cluster.instance');
if (strlen($instance)) {
- $uri->setQueryParam('instance', $instance);
+ $uri->replaceQueryParam('instance', $instance);
}
return $uri;
}
public function getWebsocketURI($to_path = null) {
$instance = PhabricatorEnv::getEnvConfig('cluster.instance');
if (strlen($instance)) {
$to_path = $to_path.'~'.$instance.'/';
}
$uri = $this->getURI($to_path);
if ($this->getProtocol() == 'https') {
$uri->setProtocol('wss');
} else {
$uri->setProtocol('ws');
}
return $uri;
}
public function testClient() {
if ($this->isAdminServer()) {
throw new Exception(
pht('Unable to test client on an admin server!'));
}
$server_uri = $this->getURI();
try {
id(new HTTPSFuture($server_uri))
->setTimeout(2)
->resolvex();
} catch (HTTPFutureHTTPResponseStatus $ex) {
// This is what we expect when things are working correctly.
if ($ex->getStatusCode() == 501) {
return true;
}
throw $ex;
}
throw new Exception(
pht('Got HTTP 200, but expected HTTP 501 (WebSocket Upgrade)!'));
}
public function loadServerStatus() {
if (!$this->isAdminServer()) {
throw new Exception(
pht(
'Unable to load server status: this is not an admin server!'));
}
$server_uri = $this->getURI('/status/');
list($body) = id(new HTTPSFuture($server_uri))
->setTimeout(2)
->resolvex();
return phutil_json_decode($body);
}
public function postMessage(array $data) {
if (!$this->isAdminServer()) {
throw new Exception(
pht('Unable to post message: this is not an admin server!'));
}
$server_uri = $this->getURI('/');
$payload = phutil_json_encode($data);
id(new HTTPSFuture($server_uri, $payload))
->setMethod('POST')
->setTimeout(2)
->resolvex();
}
}
diff --git a/src/applications/notification/controller/PhabricatorNotificationPanelController.php b/src/applications/notification/controller/PhabricatorNotificationPanelController.php
index 1e956a60e..5991e8db7 100644
--- a/src/applications/notification/controller/PhabricatorNotificationPanelController.php
+++ b/src/applications/notification/controller/PhabricatorNotificationPanelController.php
@@ -1,163 +1,163 @@
<?php
final class PhabricatorNotificationPanelController
extends PhabricatorNotificationController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$unread_count = $viewer->getUnreadNotificationCount();
$warning = $this->prunePhantomNotifications($unread_count);
$query = id(new PhabricatorNotificationQuery())
->setViewer($viewer)
->withUserPHIDs(array($viewer->getPHID()))
->setLimit(10);
$stories = $query->execute();
$clear_ui_class = 'phabricator-notification-clear-all';
$clear_uri = id(new PhutilURI('/notification/clear/'));
if ($stories) {
$builder = id(new PhabricatorNotificationBuilder($stories))
->setUser($viewer);
$notifications_view = $builder->buildView();
$content = $notifications_view->render();
- $clear_uri->setQueryParam(
+ $clear_uri->replaceQueryParam(
'chronoKey',
head($stories)->getChronologicalKey());
} else {
$content = phutil_tag_div(
'phabricator-notification no-notifications',
pht('You have no notifications.'));
$clear_ui_class .= ' disabled';
}
$clear_ui = javelin_tag(
'a',
array(
'sigil' => 'workflow',
'href' => (string)$clear_uri,
'class' => $clear_ui_class,
),
pht('Mark All Read'));
$notifications_link = phutil_tag(
'a',
array(
'href' => '/notification/',
),
pht('Notifications'));
$connection_status = new PhabricatorNotificationStatusView();
$connection_ui = phutil_tag(
'div',
array(
'class' => 'phabricator-notification-footer',
),
$connection_status);
$header = phutil_tag(
'div',
array(
'class' => 'phabricator-notification-header',
),
array(
$notifications_link,
$clear_ui,
));
$content = hsprintf(
'%s%s%s%s',
$header,
$warning,
$content,
$connection_ui);
$json = array(
'content' => $content,
'number' => (int)$unread_count,
);
return id(new AphrontAjaxResponse())->setContent($json);
}
private function prunePhantomNotifications($unread_count) {
// See T8953. If you have an unread notification about an object you
// do not have permission to view, it isn't possible to clear it by
// visiting the object. Identify these notifications and mark them as
// read.
$viewer = $this->getViewer();
if (!$unread_count) {
return null;
}
$table = new PhabricatorFeedStoryNotification();
$conn = $table->establishConnection('r');
$rows = queryfx_all(
$conn,
'SELECT chronologicalKey, primaryObjectPHID FROM %T
WHERE userPHID = %s AND hasViewed = 0',
$table->getTableName(),
$viewer->getPHID());
if (!$rows) {
return null;
}
$map = array();
foreach ($rows as $row) {
$map[$row['primaryObjectPHID']][] = $row['chronologicalKey'];
}
$handles = $viewer->loadHandles(array_keys($map));
$purge_keys = array();
foreach ($handles as $handle) {
$phid = $handle->getPHID();
if ($handle->isComplete()) {
continue;
}
foreach ($map[$phid] as $chronological_key) {
$purge_keys[] = $chronological_key;
}
}
if (!$purge_keys) {
return null;
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$conn = $table->establishConnection('w');
queryfx(
$conn,
'UPDATE %T SET hasViewed = 1
WHERE userPHID = %s AND chronologicalKey IN (%Ls)',
$table->getTableName(),
$viewer->getPHID(),
$purge_keys);
PhabricatorUserCache::clearCache(
PhabricatorUserNotificationCountCacheType::KEY_COUNT,
$viewer->getPHID());
unset($unguarded);
return phutil_tag(
'div',
array(
'class' => 'phabricator-notification phabricator-notification-warning',
),
pht(
'%s notification(s) about objects which no longer exist or which '.
'you can no longer see were discarded.',
phutil_count($purge_keys)));
}
}
diff --git a/src/applications/notification/query/PhabricatorNotificationSearchEngine.php b/src/applications/notification/query/PhabricatorNotificationSearchEngine.php
index 0ee7327bf..c7e199833 100644
--- a/src/applications/notification/query/PhabricatorNotificationSearchEngine.php
+++ b/src/applications/notification/query/PhabricatorNotificationSearchEngine.php
@@ -1,141 +1,141 @@
<?php
final class PhabricatorNotificationSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Notifications');
}
public function getApplicationClassName() {
return 'PhabricatorNotificationsApplication';
}
public function buildSavedQueryFromRequest(AphrontRequest $request) {
$saved = new PhabricatorSavedQuery();
$saved->setParameter(
'unread',
$this->readBoolFromRequest($request, 'unread'));
return $saved;
}
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$query = id(new PhabricatorNotificationQuery())
->withUserPHIDs(array($this->requireViewer()->getPHID()));
if ($saved->getParameter('unread')) {
$query->withUnread(true);
}
return $query;
}
public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved) {
$unread = $saved->getParameter('unread');
$form->appendChild(
id(new AphrontFormCheckboxControl())
->setLabel(pht('Unread'))
->addCheckbox(
'unread',
1,
pht('Show only unread notifications.'),
$unread));
}
protected function getURI($path) {
return '/notification/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array(
'all' => pht('All Notifications'),
'unread' => pht('Unread Notifications'),
);
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'all':
return $query;
case 'unread':
return $query->setParameter('unread', true);
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function renderResultList(
array $notifications,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($notifications, 'PhabricatorFeedStory');
$viewer = $this->requireViewer();
$image = id(new PHUIIconView())
->setIcon('fa-bell-o');
$button = id(new PHUIButtonView())
->setTag('a')
->addSigil('workflow')
->setColor(PHUIButtonView::GREY)
->setIcon($image)
->setText(pht('Mark All Read'));
switch ($query->getQueryKey()) {
case 'unread':
$header = pht('Unread Notifications');
$no_data = pht('You have no unread notifications.');
break;
default:
$header = pht('Notifications');
$no_data = pht('You have no notifications.');
break;
}
$clear_uri = id(new PhutilURI('/notification/clear/'));
if ($notifications) {
$builder = id(new PhabricatorNotificationBuilder($notifications))
->setUser($viewer);
$view = $builder->buildView();
- $clear_uri->setQueryParam(
+ $clear_uri->replaceQueryParam(
'chronoKey',
head($notifications)->getChronologicalKey());
} else {
$view = phutil_tag_div(
'phabricator-notification no-notifications',
$no_data);
$button->setDisabled(true);
}
$button->setHref((string)$clear_uri);
$view = id(new PHUIBoxView())
->addPadding(PHUI::PADDING_MEDIUM)
->addClass('phabricator-notification-list')
->appendChild($view);
$result = new PhabricatorApplicationSearchResultView();
$result->addAction($button);
$result->setContent($view);
return $result;
}
public function shouldUseOffsetPaging() {
return true;
}
}
diff --git a/src/applications/oauthserver/PhabricatorOAuthResponse.php b/src/applications/oauthserver/PhabricatorOAuthResponse.php
index 62c0fc982..e0fca827b 100644
--- a/src/applications/oauthserver/PhabricatorOAuthResponse.php
+++ b/src/applications/oauthserver/PhabricatorOAuthResponse.php
@@ -1,106 +1,106 @@
<?php
final class PhabricatorOAuthResponse extends AphrontResponse {
private $state;
private $content;
private $clientURI;
private $error;
private $errorDescription;
private function getState() {
return $this->state;
}
public function setState($state) {
$this->state = $state;
return $this;
}
private function getContent() {
return $this->content;
}
public function setContent($content) {
$this->content = $content;
return $this;
}
private function getClientURI() {
return $this->clientURI;
}
public function setClientURI(PhutilURI $uri) {
$this->setHTTPResponseCode(302);
$this->clientURI = $uri;
return $this;
}
private function getFullURI() {
$base_uri = $this->getClientURI();
$query_params = $this->buildResponseDict();
foreach ($query_params as $key => $value) {
- $base_uri->setQueryParam($key, $value);
+ $base_uri->replaceQueryParam($key, $value);
}
return $base_uri;
}
private function getError() {
return $this->error;
}
public function setError($error) {
// errors sometimes redirect to the client (302) but otherwise
// the spec says all code 400
if (!$this->getClientURI()) {
$this->setHTTPResponseCode(400);
}
$this->error = $error;
return $this;
}
private function getErrorDescription() {
return $this->errorDescription;
}
public function setErrorDescription($error_description) {
$this->errorDescription = $error_description;
return $this;
}
public function __construct() {
$this->setHTTPResponseCode(200); // assume the best
}
public function getHeaders() {
$headers = array(
array('Content-Type', 'application/json'),
);
if ($this->getClientURI()) {
$headers[] = array('Location', $this->getFullURI());
}
// TODO -- T844 set headers with X-Auth-Scopes, etc
$headers = array_merge(parent::getHeaders(), $headers);
return $headers;
}
private function buildResponseDict() {
if ($this->getError()) {
$content = array(
'error' => $this->getError(),
'error_description' => $this->getErrorDescription(),
);
$this->setContent($content);
}
$content = $this->getContent();
if (!$content) {
return '';
}
if ($this->getState()) {
$content['state'] = $this->getState();
}
return $content;
}
public function buildResponseString() {
return $this->encodeJSONForHTTPResponse($this->buildResponseDict());
}
}
diff --git a/src/applications/oauthserver/PhabricatorOAuthServer.php b/src/applications/oauthserver/PhabricatorOAuthServer.php
index f5c074f4e..889e96021 100644
--- a/src/applications/oauthserver/PhabricatorOAuthServer.php
+++ b/src/applications/oauthserver/PhabricatorOAuthServer.php
@@ -1,284 +1,284 @@
<?php
/**
* Implements core OAuth 2.0 Server logic.
*
* This class should be used behind business logic that parses input to
* determine pertinent @{class:PhabricatorUser} $user,
* @{class:PhabricatorOAuthServerClient} $client(s),
* @{class:PhabricatorOAuthServerAuthorizationCode} $code(s), and.
* @{class:PhabricatorOAuthServerAccessToken} $token(s).
*
* For an OAuth 2.0 server, there are two main steps:
*
* 1) Authorization - the user authorizes a given client to access the data
* the OAuth 2.0 server protects. Once this is achieved / if it has
* been achived already, the OAuth server sends the client an authorization
* code.
* 2) Access Token - the client should send the authorization code received in
* step 1 along with its id and secret to the OAuth server to receive an
* access token. This access token can later be used to access Phabricator
* data on behalf of the user.
*
* @task auth Authorizing @{class:PhabricatorOAuthServerClient}s and
* generating @{class:PhabricatorOAuthServerAuthorizationCode}s
* @task token Validating @{class:PhabricatorOAuthServerAuthorizationCode}s
* and generating @{class:PhabricatorOAuthServerAccessToken}s
* @task internal Internals
*/
final class PhabricatorOAuthServer extends Phobject {
const AUTHORIZATION_CODE_TIMEOUT = 300;
private $user;
private $client;
private function getUser() {
if (!$this->user) {
throw new PhutilInvalidStateException('setUser');
}
return $this->user;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
private function getClient() {
if (!$this->client) {
throw new PhutilInvalidStateException('setClient');
}
return $this->client;
}
public function setClient(PhabricatorOAuthServerClient $client) {
$this->client = $client;
return $this;
}
/**
* @task auth
* @return tuple <bool hasAuthorized, ClientAuthorization or null>
*/
public function userHasAuthorizedClient(array $scope) {
$authorization = id(new PhabricatorOAuthClientAuthorization())
->loadOneWhere(
'userPHID = %s AND clientPHID = %s',
$this->getUser()->getPHID(),
$this->getClient()->getPHID());
if (empty($authorization)) {
return array(false, null);
}
if ($scope) {
$missing_scope = array_diff_key($scope, $authorization->getScope());
} else {
$missing_scope = false;
}
if ($missing_scope) {
return array(false, $authorization);
}
return array(true, $authorization);
}
/**
* @task auth
*/
public function authorizeClient(array $scope) {
$authorization = new PhabricatorOAuthClientAuthorization();
$authorization->setUserPHID($this->getUser()->getPHID());
$authorization->setClientPHID($this->getClient()->getPHID());
$authorization->setScope($scope);
$authorization->save();
return $authorization;
}
/**
* @task auth
*/
public function generateAuthorizationCode(PhutilURI $redirect_uri) {
$code = Filesystem::readRandomCharacters(32);
$client = $this->getClient();
$authorization_code = new PhabricatorOAuthServerAuthorizationCode();
$authorization_code->setCode($code);
$authorization_code->setClientPHID($client->getPHID());
$authorization_code->setClientSecret($client->getSecret());
$authorization_code->setUserPHID($this->getUser()->getPHID());
$authorization_code->setRedirectURI((string)$redirect_uri);
$authorization_code->save();
return $authorization_code;
}
/**
* @task token
*/
public function generateAccessToken() {
$token = Filesystem::readRandomCharacters(32);
$access_token = new PhabricatorOAuthServerAccessToken();
$access_token->setToken($token);
$access_token->setUserPHID($this->getUser()->getPHID());
$access_token->setClientPHID($this->getClient()->getPHID());
$access_token->save();
return $access_token;
}
/**
* @task token
*/
public function validateAuthorizationCode(
PhabricatorOAuthServerAuthorizationCode $test_code,
PhabricatorOAuthServerAuthorizationCode $valid_code) {
// check that all the meta data matches
if ($test_code->getClientPHID() != $valid_code->getClientPHID()) {
return false;
}
if ($test_code->getClientSecret() != $valid_code->getClientSecret()) {
return false;
}
// check that the authorization code hasn't timed out
$created_time = $test_code->getDateCreated();
$must_be_used_by = $created_time + self::AUTHORIZATION_CODE_TIMEOUT;
return (time() < $must_be_used_by);
}
/**
* @task token
*/
public function authorizeToken(
PhabricatorOAuthServerAccessToken $token) {
$user_phid = $token->getUserPHID();
$client_phid = $token->getClientPHID();
$authorization = id(new PhabricatorOAuthClientAuthorizationQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUserPHIDs(array($user_phid))
->withClientPHIDs(array($client_phid))
->executeOne();
if (!$authorization) {
return null;
}
$application = $authorization->getClient();
if ($application->getIsDisabled()) {
return null;
}
return $authorization;
}
public function validateRedirectURI($uri) {
try {
$this->assertValidRedirectURI($uri);
return true;
} catch (Exception $ex) {
return false;
}
}
/**
* See http://tools.ietf.org/html/draft-ietf-oauth-v2-23#section-3.1.2
* for details on what makes a given redirect URI "valid".
*/
public function assertValidRedirectURI($raw_uri) {
// This covers basics like reasonable formatting and the existence of a
// protocol.
PhabricatorEnv::requireValidRemoteURIForLink($raw_uri);
$uri = new PhutilURI($raw_uri);
$fragment = $uri->getFragment();
if (strlen($fragment)) {
throw new Exception(
pht(
'OAuth application redirect URIs must not contain URI '.
'fragments, but the URI "%s" has a fragment ("%s").',
$raw_uri,
$fragment));
}
$protocol = $uri->getProtocol();
switch ($protocol) {
case 'http':
case 'https':
break;
default:
throw new Exception(
pht(
'OAuth application redirect URIs must only use the "http" or '.
'"https" protocols, but the URI "%s" uses the "%s" protocol.',
$raw_uri,
$protocol));
}
}
/**
* If there's a URI specified in an OAuth request, it must be validated in
* its own right. Further, it must have the same domain, the same path, the
* same port, and (at least) the same query parameters as the primary URI.
*/
public function validateSecondaryRedirectURI(
PhutilURI $secondary_uri,
PhutilURI $primary_uri) {
// The secondary URI must be valid.
if (!$this->validateRedirectURI($secondary_uri)) {
return false;
}
// Both URIs must point at the same domain.
if ($secondary_uri->getDomain() != $primary_uri->getDomain()) {
return false;
}
// Both URIs must have the same path
if ($secondary_uri->getPath() != $primary_uri->getPath()) {
return false;
}
// Both URIs must have the same port
if ($secondary_uri->getPort() != $primary_uri->getPort()) {
return false;
}
// Any query parameters present in the first URI must be exactly present
// in the second URI.
- $need_params = $primary_uri->getQueryParams();
- $have_params = $secondary_uri->getQueryParams();
+ $need_params = $primary_uri->getQueryParamsAsMap();
+ $have_params = $secondary_uri->getQueryParamsAsMap();
foreach ($need_params as $key => $value) {
if (!array_key_exists($key, $have_params)) {
return false;
}
if ((string)$have_params[$key] != (string)$value) {
return false;
}
}
// If the first URI is HTTPS, the second URI must also be HTTPS. This
// defuses an attack where a third party with control over the network
// tricks you into using HTTP to authenticate over a link which is supposed
// to be HTTPS only and sniffs all your token cookies.
if (strtolower($primary_uri->getProtocol()) == 'https') {
if (strtolower($secondary_uri->getProtocol()) != 'https') {
return false;
}
}
return true;
}
}
diff --git a/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php b/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php
index 745be3e82..2b454e00e 100644
--- a/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php
+++ b/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php
@@ -1,316 +1,316 @@
<?php
final class PhabricatorOAuthServerAuthController
extends PhabricatorOAuthServerController {
protected function buildApplicationCrumbs() {
// We're specifically not putting an "OAuth Server" application crumb
// on the auth pages because it doesn't make sense to send users there.
return new PHUICrumbsView();
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$server = new PhabricatorOAuthServer();
$client_phid = $request->getStr('client_id');
$redirect_uri = $request->getStr('redirect_uri');
$response_type = $request->getStr('response_type');
// state is an opaque value the client sent us for their own purposes
// we just need to send it right back to them in the response!
$state = $request->getStr('state');
if (!$client_phid) {
return $this->buildErrorResponse(
'invalid_request',
pht('Malformed Request'),
pht(
'Required parameter %s was not present in the request.',
phutil_tag('strong', array(), 'client_id')));
}
// We require that users must be able to see an OAuth application
// in order to authorize it. This allows an application's visibility
// policy to be used to restrict authorized users.
try {
$client = id(new PhabricatorOAuthServerClientQuery())
->setViewer($viewer)
->withPHIDs(array($client_phid))
->executeOne();
} catch (PhabricatorPolicyException $ex) {
$ex->setContext(self::CONTEXT_AUTHORIZE);
throw $ex;
}
$server->setUser($viewer);
$is_authorized = false;
$authorization = null;
$uri = null;
$name = null;
// one giant try / catch around all the exciting database stuff so we
// can return a 'server_error' response if something goes wrong!
try {
if (!$client) {
return $this->buildErrorResponse(
'invalid_request',
pht('Invalid Client Application'),
pht(
'Request parameter %s does not specify a valid client application.',
phutil_tag('strong', array(), 'client_id')));
}
if ($client->getIsDisabled()) {
return $this->buildErrorResponse(
'invalid_request',
pht('Application Disabled'),
pht(
'The %s OAuth application has been disabled.',
phutil_tag('strong', array(), 'client_id')));
}
$name = $client->getName();
$server->setClient($client);
if ($redirect_uri) {
$client_uri = new PhutilURI($client->getRedirectURI());
$redirect_uri = new PhutilURI($redirect_uri);
if (!($server->validateSecondaryRedirectURI($redirect_uri,
$client_uri))) {
return $this->buildErrorResponse(
'invalid_request',
pht('Invalid Redirect URI'),
pht(
'Request parameter %s specifies an invalid redirect URI. '.
'The redirect URI must be a fully-qualified domain with no '.
'fragments, and must have the same domain and at least '.
'the same query parameters as the redirect URI the client '.
'registered.',
phutil_tag('strong', array(), 'redirect_uri')));
}
$uri = $redirect_uri;
} else {
$uri = new PhutilURI($client->getRedirectURI());
}
if (empty($response_type)) {
return $this->buildErrorResponse(
'invalid_request',
pht('Invalid Response Type'),
pht(
'Required request parameter %s is missing.',
phutil_tag('strong', array(), 'response_type')));
}
if ($response_type != 'code') {
return $this->buildErrorResponse(
'unsupported_response_type',
pht('Unsupported Response Type'),
pht(
'Request parameter %s specifies an unsupported response type. '.
'Valid response types are: %s.',
phutil_tag('strong', array(), 'response_type'),
implode(', ', array('code'))));
}
$requested_scope = $request->getStrList('scope');
$requested_scope = array_fuse($requested_scope);
$scope = PhabricatorOAuthServerScope::filterScope($requested_scope);
// NOTE: We're always requiring a confirmation dialog to redirect.
// Partly this is a general defense against redirect attacks, and
// partly this shakes off anchors in the URI (which are not shaken
// by 302'ing).
$auth_info = $server->userHasAuthorizedClient($scope);
list($is_authorized, $authorization) = $auth_info;
if ($request->isFormPost()) {
if ($authorization) {
$authorization->setScope($scope)->save();
} else {
$authorization = $server->authorizeClient($scope);
}
$is_authorized = true;
}
} catch (Exception $e) {
return $this->buildErrorResponse(
'server_error',
pht('Server Error'),
pht(
'The authorization server encountered an unexpected condition '.
'which prevented it from fulfilling the request.'));
}
// When we reach this part of the controller, we can be in two states:
//
// 1. The user has not authorized the application yet. We want to
// give them an "Authorize this application?" dialog.
// 2. The user has authorized the application. We want to give them
// a "Confirm Login" dialog.
if ($is_authorized) {
// The second case is simpler, so handle it first. The user either
// authorized the application previously, or has just authorized the
// application. Show them a confirm dialog with a normal link back to
// the application. This shakes anchors from the URI.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$auth_code = $server->generateAuthorizationCode($uri);
unset($unguarded);
$full_uri = $this->addQueryParams(
$uri,
array(
'code' => $auth_code->getCode(),
'scope' => $authorization->getScopeString(),
'state' => $state,
));
if ($client->getIsTrusted()) {
// NOTE: See T13099. We currently emit a "Content-Security-Policy"
// which includes a narrow "form-action". At the time of writing,
// Chrome applies "form-action" to redirects following form submission.
// This can lead to a situation where a user enters the OAuth workflow
// and is prompted for MFA. When they submit an MFA response, the form
// can redirect here, and Chrome will block the "Location" redirect.
// To avoid this, render an interstitial. We only actually need to do
// this in Chrome (but do it everywhere for consistency) and only need
// to do it if the request is a redirect after a form submission (but
// we can't tell if it is or not).
Javelin::initBehavior(
'redirect',
array(
'uri' => (string)$full_uri,
));
return $this->newDialog()
->setTitle(pht('Authenticate: %s', $name))
->appendParagraph(
pht(
'Authorization for "%s" confirmed, redirecting...',
phutil_tag('strong', array(), $name)))
->addCancelButton((string)$full_uri, pht('Continue'));
}
// TODO: It would be nice to give the user more options here, like
// reviewing permissions, canceling the authorization, or aborting
// the workflow.
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('Authenticate: %s', $name))
->appendParagraph(
pht(
'This application ("%s") is authorized to use your Phabricator '.
'credentials. Continue to complete the authentication workflow.',
phutil_tag('strong', array(), $name)))
->addCancelButton((string)$full_uri, pht('Continue to Application'));
return id(new AphrontDialogResponse())->setDialog($dialog);
}
// Here, we're confirming authorization for the application.
if ($authorization) {
$missing_scope = array_diff_key($scope, $authorization->getScope());
} else {
$missing_scope = $scope;
}
$form = id(new AphrontFormView())
->addHiddenInput('client_id', $client_phid)
->addHiddenInput('redirect_uri', $redirect_uri)
->addHiddenInput('response_type', $response_type)
->addHiddenInput('state', $state)
->addHiddenInput('scope', $request->getStr('scope'))
->setUser($viewer);
$cancel_msg = pht('The user declined to authorize this application.');
$cancel_uri = $this->addQueryParams(
$uri,
array(
'error' => 'access_denied',
'error_description' => $cancel_msg,
));
$dialog = $this->newDialog()
->setShortTitle(pht('Authorize Access'))
->setTitle(pht('Authorize "%s"?', $name))
->setSubmitURI($request->getRequestURI()->getPath())
->setWidth(AphrontDialogView::WIDTH_FORM)
->appendParagraph(
pht(
'Do you want to authorize the external application "%s" to '.
'access your Phabricator account data, including your primary '.
'email address?',
phutil_tag('strong', array(), $name)))
->appendForm($form)
->addSubmitButton(pht('Authorize Access'))
->addCancelButton((string)$cancel_uri, pht('Do Not Authorize'));
if ($missing_scope) {
$dialog->appendParagraph(
pht(
'This application has requested these additional permissions. '.
'Authorizing it will grant it the permissions it requests:'));
foreach ($missing_scope as $scope_key => $ignored) {
// TODO: Once we introduce more scopes, explain them here.
}
}
$unknown_scope = array_diff_key($requested_scope, $scope);
if ($unknown_scope) {
$dialog->appendParagraph(
pht(
'This application also requested additional unrecognized '.
'permissions. These permissions may have existed in an older '.
'version of Phabricator, or may be from a future version of '.
'Phabricator. They will not be granted.'));
$unknown_form = id(new AphrontFormView())
->setViewer($viewer)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Unknown Scope'))
->setValue(implode(', ', array_keys($unknown_scope)))
->setDisabled(true));
$dialog->appendForm($unknown_form);
}
return $dialog;
}
private function buildErrorResponse($code, $title, $message) {
$viewer = $this->getRequest()->getUser();
return $this->newDialog()
->setTitle(pht('OAuth: %s', $title))
->appendParagraph($message)
->appendParagraph(
pht('OAuth Error Code: %s', phutil_tag('tt', array(), $code)))
->addCancelButton('/', pht('Alas!'));
}
private function addQueryParams(PhutilURI $uri, array $params) {
$full_uri = clone $uri;
foreach ($params as $key => $value) {
if (strlen($value)) {
- $full_uri->setQueryParam($key, $value);
+ $full_uri->replaceQueryParam($key, $value);
}
}
return $full_uri;
}
}
diff --git a/src/applications/oauthserver/storage/PhabricatorOAuthServerTransaction.php b/src/applications/oauthserver/storage/PhabricatorOAuthServerTransaction.php
index b2624dd9a..acfb88ef4 100644
--- a/src/applications/oauthserver/storage/PhabricatorOAuthServerTransaction.php
+++ b/src/applications/oauthserver/storage/PhabricatorOAuthServerTransaction.php
@@ -1,63 +1,59 @@
<?php
final class PhabricatorOAuthServerTransaction
extends PhabricatorApplicationTransaction {
const TYPE_NAME = 'oauthserver.name';
const TYPE_REDIRECT_URI = 'oauthserver.redirect-uri';
const TYPE_DISABLED = 'oauthserver.disabled';
public function getApplicationName() {
return 'oauth_server';
}
public function getTableName() {
return 'oauth_server_transaction';
}
public function getApplicationTransactionType() {
return PhabricatorOAuthServerClientPHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return null;
- }
-
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CREATE:
return pht(
'%s created this OAuth application.',
$this->renderHandleLink($author_phid));
case self::TYPE_NAME:
return pht(
'%s renamed this application from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$old,
$new);
case self::TYPE_REDIRECT_URI:
return pht(
'%s changed the application redirect URI from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$old,
$new);
case self::TYPE_DISABLED:
if ($new) {
return pht(
'%s disabled this application.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s enabled this application.',
$this->renderHandleLink($author_phid));
}
}
return parent::getTitle();
}
}
diff --git a/src/applications/owners/constants/PhabricatorOwnersAuditRule.php b/src/applications/owners/constants/PhabricatorOwnersAuditRule.php
new file mode 100644
index 000000000..32a9bb804
--- /dev/null
+++ b/src/applications/owners/constants/PhabricatorOwnersAuditRule.php
@@ -0,0 +1,117 @@
+<?php
+
+final class PhabricatorOwnersAuditRule
+ extends Phobject {
+
+ const AUDITING_NONE = 'none';
+ const AUDITING_NO_OWNER = 'audit';
+ const AUDITING_UNREVIEWED = 'unreviewed';
+ const AUDITING_NO_OWNER_AND_UNREVIEWED = 'uninvolved-unreviewed';
+ const AUDITING_ALL = 'all';
+
+ private $key;
+ private $spec;
+
+ public static function newFromState($key) {
+ $specs = self::newSpecifications();
+ $spec = idx($specs, $key, array());
+
+ $rule = new self();
+ $rule->key = $key;
+ $rule->spec = $spec;
+
+ return $rule;
+ }
+
+ public function getKey() {
+ return $this->key;
+ }
+
+ public function getDisplayName() {
+ return idx($this->spec, 'name', $this->key);
+ }
+
+ public function getIconIcon() {
+ return idx($this->spec, 'icon.icon');
+ }
+
+ public static function newSelectControlMap() {
+ $specs = self::newSpecifications();
+ return ipull($specs, 'name');
+ }
+
+ public static function getStorageValueFromAPIValue($value) {
+ $specs = self::newSpecifications();
+
+ $map = array();
+ foreach ($specs as $key => $spec) {
+ $deprecated = idx($spec, 'deprecated', array());
+ if (isset($deprecated[$value])) {
+ return $key;
+ }
+ }
+
+ return $value;
+ }
+
+ public static function getModernValueMap() {
+ $specs = self::newSpecifications();
+
+ $map = array();
+ foreach ($specs as $key => $spec) {
+ $map[$key] = pht('"%s"', $key);
+ }
+
+ return $map;
+ }
+
+ public static function getDeprecatedValueMap() {
+ $specs = self::newSpecifications();
+
+ $map = array();
+ foreach ($specs as $key => $spec) {
+ $deprecated_map = idx($spec, 'deprecated', array());
+ foreach ($deprecated_map as $deprecated_key => $label) {
+ $map[$deprecated_key] = $label;
+ }
+ }
+
+ return $map;
+ }
+
+ private static function newSpecifications() {
+ return array(
+ self::AUDITING_NONE => array(
+ 'name' => pht('No Auditing'),
+ 'icon.icon' => 'fa-ban',
+ 'deprecated' => array(
+ '' => pht('"" (empty string)'),
+ '0' => '"0"',
+ ),
+ ),
+ self::AUDITING_UNREVIEWED => array(
+ 'name' => pht('Audit Unreviewed Commits'),
+ 'icon.icon' => 'fa-check',
+ ),
+ self::AUDITING_NO_OWNER => array(
+ 'name' => pht('Audit Commits With No Owner Involvement'),
+ 'icon.icon' => 'fa-check',
+ 'deprecated' => array(
+ '1' => '"1"',
+ ),
+ ),
+ self::AUDITING_NO_OWNER_AND_UNREVIEWED => array(
+ 'name' => pht(
+ 'Audit Unreviewed Commits and Commits With No Owner Involvement'),
+ 'icon.icon' => 'fa-check',
+ ),
+ self::AUDITING_ALL => array(
+ 'name' => pht('Audit All Commits'),
+ 'icon.icon' => 'fa-check',
+ ),
+ );
+ }
+
+
+
+}
diff --git a/src/applications/owners/controller/PhabricatorOwnersDetailController.php b/src/applications/owners/controller/PhabricatorOwnersDetailController.php
index f71009cf1..c458e4dbd 100644
--- a/src/applications/owners/controller/PhabricatorOwnersDetailController.php
+++ b/src/applications/owners/controller/PhabricatorOwnersDetailController.php
@@ -1,352 +1,348 @@
<?php
final class PhabricatorOwnersDetailController
extends PhabricatorOwnersController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$package = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->needPaths(true)
->executeOne();
if (!$package) {
return new Aphront404Response();
}
$paths = $package->getPaths();
$repository_phids = array();
foreach ($paths as $path) {
$repository_phids[$path->getRepositoryPHID()] = true;
}
if ($repository_phids) {
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withPHIDs(array_keys($repository_phids))
->execute();
$repositories = mpull($repositories, null, 'getPHID');
} else {
$repositories = array();
}
$field_list = PhabricatorCustomField::getObjectFields(
$package,
PhabricatorCustomField::ROLE_VIEW);
$field_list
->setViewer($viewer)
->readFieldsFromStorage($package);
$curtain = $this->buildCurtain($package);
$details = $this->buildPackageDetailView($package, $field_list);
if ($package->isArchived()) {
$header_icon = 'fa-ban';
$header_name = pht('Archived');
$header_color = 'dark';
} else {
$header_icon = 'fa-check';
$header_name = pht('Active');
$header_color = 'bluegrey';
}
$header = id(new PHUIHeaderView())
->setUser($viewer)
->setHeader($package->getName())
->setStatus($header_icon, $header_color, $header_name)
->setPolicyObject($package)
->setHeaderIcon('fa-gift');
$commit_views = array();
- $commit_uri = id(new PhutilURI('/diffusion/commit/'))
- ->setQueryParams(
- array(
- 'package' => $package->getPHID(),
- ));
+ $params = array(
+ 'package' => $package->getPHID(),
+ );
+
+ $commit_uri = new PhutilURI('/diffusion/commit/', $params);
$status_concern = DiffusionCommitAuditStatus::CONCERN_RAISED;
$attention_commits = id(new DiffusionCommitQuery())
->setViewer($request->getUser())
->withPackagePHIDs(array($package->getPHID()))
->withStatuses(
array(
$status_concern,
))
->needCommitData(true)
->needAuditRequests(true)
->setLimit(10)
->execute();
$view = id(new PhabricatorAuditListView())
->setUser($viewer)
->setNoDataString(pht('This package has no open problem commits.'))
->setCommits($attention_commits);
$commit_views[] = array(
'view' => $view,
'header' => pht('Needs Attention'),
'icon' => 'fa-warning',
'button' => id(new PHUIButtonView())
->setTag('a')
->setHref($commit_uri->alter('status', $status_concern))
->setIcon('fa-list-ul')
->setText(pht('View All')),
);
$all_commits = id(new DiffusionCommitQuery())
->setViewer($request->getUser())
->withPackagePHIDs(array($package->getPHID()))
->needCommitData(true)
->needAuditRequests(true)
->setLimit(25)
->execute();
$view = id(new PhabricatorAuditListView())
->setUser($viewer)
->setCommits($all_commits)
->setNoDataString(pht('No commits in this package.'));
$commit_views[] = array(
'view' => $view,
'header' => pht('Recent Commits'),
'icon' => 'fa-code',
'button' => id(new PHUIButtonView())
->setTag('a')
->setHref($commit_uri)
->setIcon('fa-list-ul')
->setText(pht('View All')),
);
$commit_panels = array();
foreach ($commit_views as $commit_view) {
$commit_panel = id(new PHUIObjectBoxView())
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
$commit_header = id(new PHUIHeaderView())
->setHeader($commit_view['header'])
->setHeaderIcon($commit_view['icon']);
if (isset($commit_view['button'])) {
$commit_header->addActionLink($commit_view['button']);
}
$commit_panel->setHeader($commit_header);
$commit_panel->appendChild($commit_view['view']);
$commit_panels[] = $commit_panel;
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($package->getMonogram());
$crumbs->setBorder(true);
$timeline = $this->buildTransactionTimeline(
$package,
new PhabricatorOwnersPackageTransactionQuery());
$timeline->setShouldTerminate(true);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(array(
$this->renderPathsTable($paths, $repositories),
$commit_panels,
$timeline,
))
->addPropertySection(pht('Details'), $details);
return $this->newPage()
->setTitle($package->getName())
->setCrumbs($crumbs)
->appendChild($view);
}
private function buildPackageDetailView(
PhabricatorOwnersPackage $package,
PhabricatorCustomFieldList $field_list) {
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setUser($viewer);
$owners = $package->getOwners();
if ($owners) {
$owner_list = $viewer->renderHandleList(mpull($owners, 'getUserPHID'));
} else {
$owner_list = phutil_tag('em', array(), pht('None'));
}
$view->addProperty(pht('Owners'), $owner_list);
$dominion = $package->getDominion();
$dominion_map = PhabricatorOwnersPackage::getDominionOptionsMap();
$spec = idx($dominion_map, $dominion, array());
$name = idx($spec, 'short', $dominion);
$view->addProperty(pht('Dominion'), $name);
$auto = $package->getAutoReview();
$autoreview_map = PhabricatorOwnersPackage::getAutoreviewOptionsMap();
$spec = idx($autoreview_map, $auto, array());
$name = idx($spec, 'name', $auto);
$view->addProperty(pht('Auto Review'), $name);
- if ($package->getAuditingEnabled()) {
- $auditing = pht('Enabled');
- } else {
- $auditing = pht('Disabled');
- }
- $view->addProperty(pht('Auditing'), $auditing);
+ $rule = $package->newAuditingRule();
+ $view->addProperty(pht('Auditing'), $rule->getDisplayName());
$ignored = $package->getIgnoredPathAttributes();
$ignored = array_keys($ignored);
if ($ignored) {
$ignored = implode(', ', $ignored);
} else {
$ignored = phutil_tag('em', array(), pht('None'));
}
$view->addProperty(pht('Ignored Attributes'), $ignored);
$description = $package->getDescription();
if (strlen($description)) {
$description = new PHUIRemarkupView($viewer, $description);
$view->addSectionHeader(pht('Description'));
$view->addTextContent($description);
}
$field_list->appendFieldsToPropertyList(
$package,
$viewer,
$view);
return $view;
}
private function buildCurtain(PhabricatorOwnersPackage $package) {
$viewer = $this->getViewer();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$package,
PhabricatorPolicyCapability::CAN_EDIT);
$id = $package->getID();
$edit_uri = $this->getApplicationURI("/edit/{$id}/");
$paths_uri = $this->getApplicationURI("/paths/{$id}/");
$curtain = $this->newCurtainView($package);
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Package'))
->setIcon('fa-pencil')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setHref($edit_uri));
if ($package->isArchived()) {
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Activate Package'))
->setIcon('fa-check')
->setDisabled(!$can_edit)
->setWorkflow($can_edit)
->setHref($this->getApplicationURI("/archive/{$id}/")));
} else {
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Archive Package'))
->setIcon('fa-ban')
->setDisabled(!$can_edit)
->setWorkflow($can_edit)
->setHref($this->getApplicationURI("/archive/{$id}/")));
}
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Paths'))
->setIcon('fa-folder-open')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setHref($paths_uri));
return $curtain;
}
private function renderPathsTable(array $paths, array $repositories) {
$viewer = $this->getViewer();
$rows = array();
foreach ($paths as $path) {
$repo = idx($repositories, $path->getRepositoryPHID());
if (!$repo) {
continue;
}
$href = $repo->generateURI(
array(
'branch' => $repo->getDefaultBranch(),
'path' => $path->getPathDisplay(),
'action' => 'browse',
));
$path_link = phutil_tag(
'a',
array(
'href' => (string)$href,
),
$path->getPathDisplay());
$rows[] = array(
($path->getExcluded() ? '-' : '+'),
$repo->getName(),
$path_link,
);
}
$info = null;
if (!$paths) {
$info = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(
array(
pht(
'This package does not contain any paths yet. Use '.
'"Edit Paths" to add some.'),
));
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
null,
pht('Repository'),
pht('Path'),
))
->setColumnClasses(
array(
null,
null,
'wide',
));
if ($info) {
$table->setNotice($info);
}
$header = id(new PHUIHeaderView())
->setHeader(pht('Paths'))
->setHeaderIcon('fa-folder-open');
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($table);
return $box;
}
}
diff --git a/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php b/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php
index 044cb8bed..13f896d3f 100644
--- a/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php
+++ b/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php
@@ -1,190 +1,186 @@
<?php
final class PhabricatorOwnersPackageEditEngine
extends PhabricatorEditEngine {
const ENGINECONST = 'owners.package';
public function getEngineName() {
return pht('Owners Packages');
}
public function getSummaryHeader() {
return pht('Configure Owners Package Forms');
}
public function getSummaryText() {
return pht('Configure forms for creating and editing packages in Owners.');
}
public function getEngineApplicationClass() {
return 'PhabricatorOwnersApplication';
}
protected function newEditableObject() {
return PhabricatorOwnersPackage::initializeNewPackage($this->getViewer());
}
protected function newObjectQuery() {
return id(new PhabricatorOwnersPackageQuery())
->needPaths(true);
}
protected function getObjectCreateTitleText($object) {
return pht('Create New Package');
}
protected function getObjectEditTitleText($object) {
return pht('Edit Package: %s', $object->getName());
}
protected function getObjectEditShortText($object) {
return pht('Package %d', $object->getID());
}
protected function getObjectCreateShortText() {
return pht('Create Package');
}
protected function getObjectName() {
return pht('Package');
}
protected function getObjectViewURI($object) {
return $object->getURI();
}
protected function buildCustomEditFields($object) {
$paths_help = pht(<<<EOTEXT
When updating the paths for a package, pass a list of dictionaries like
this as the `value` for the transaction:
```lang=json, name="Example Paths Value"
[
{
"repositoryPHID": "PHID-REPO-1234",
"path": "/path/to/directory/",
"excluded": false
},
{
"repositoryPHID": "PHID-REPO-1234",
"path": "/another/example/path/",
"excluded": false
}
]
```
This transaction will set the paths to the list you provide, overwriting any
previous paths.
Generally, you will call `owners.search` first to get a list of current paths
(which are provided in the same format), make changes, then update them by
applying a transaction of this type.
EOTEXT
);
$autoreview_map = PhabricatorOwnersPackage::getAutoreviewOptionsMap();
$autoreview_map = ipull($autoreview_map, 'name');
$dominion_map = PhabricatorOwnersPackage::getDominionOptionsMap();
$dominion_map = ipull($dominion_map, 'name');
return array(
id(new PhabricatorTextEditField())
->setKey('name')
->setLabel(pht('Name'))
->setDescription(pht('Name of the package.'))
->setTransactionType(
PhabricatorOwnersPackageNameTransaction::TRANSACTIONTYPE)
->setIsRequired(true)
->setValue($object->getName()),
id(new PhabricatorDatasourceEditField())
->setKey('owners')
->setLabel(pht('Owners'))
->setDescription(pht('Users and projects which own the package.'))
->setTransactionType(
PhabricatorOwnersPackageOwnersTransaction::TRANSACTIONTYPE)
->setDatasource(new PhabricatorProjectOrUserDatasource())
->setIsCopyable(true)
->setValue($object->getOwnerPHIDs()),
id(new PhabricatorSelectEditField())
->setKey('dominion')
->setLabel(pht('Dominion'))
->setDescription(
pht('Change package dominion rules.'))
->setTransactionType(
PhabricatorOwnersPackageDominionTransaction::TRANSACTIONTYPE)
->setIsCopyable(true)
->setValue($object->getDominion())
->setOptions($dominion_map),
id(new PhabricatorSelectEditField())
->setKey('autoReview')
->setLabel(pht('Auto Review'))
->setDescription(
pht(
'Automatically trigger reviews for commits affecting files in '.
'this package.'))
->setTransactionType(
PhabricatorOwnersPackageAutoreviewTransaction::TRANSACTIONTYPE)
->setIsCopyable(true)
->setValue($object->getAutoReview())
->setOptions($autoreview_map),
id(new PhabricatorSelectEditField())
->setKey('auditing')
->setLabel(pht('Auditing'))
->setDescription(
pht(
'Automatically trigger audits for commits affecting files in '.
'this package.'))
->setTransactionType(
PhabricatorOwnersPackageAuditingTransaction::TRANSACTIONTYPE)
->setIsCopyable(true)
- ->setValue($object->getAuditingEnabled())
- ->setOptions(
- array(
- '' => pht('Disabled'),
- '1' => pht('Enabled'),
- )),
+ ->setValue($object->getAuditingState())
+ ->setOptions(PhabricatorOwnersAuditRule::newSelectControlMap()),
id(new PhabricatorRemarkupEditField())
->setKey('description')
->setLabel(pht('Description'))
->setDescription(pht('Human-readable description of the package.'))
->setTransactionType(
PhabricatorOwnersPackageDescriptionTransaction::TRANSACTIONTYPE)
->setValue($object->getDescription()),
id(new PhabricatorSelectEditField())
->setKey('status')
->setLabel(pht('Status'))
->setDescription(pht('Archive or enable the package.'))
->setTransactionType(
PhabricatorOwnersPackageStatusTransaction::TRANSACTIONTYPE)
->setIsFormField(false)
->setValue($object->getStatus())
->setOptions($object->getStatusNameMap()),
id(new PhabricatorCheckboxesEditField())
->setKey('ignored')
->setLabel(pht('Ignored Attributes'))
->setDescription(pht('Ignore paths with any of these attributes.'))
->setTransactionType(
PhabricatorOwnersPackageIgnoredTransaction::TRANSACTIONTYPE)
->setValue(array_keys($object->getIgnoredPathAttributes()))
->setOptions(
array(
'generated' => pht('Ignore generated files (review only).'),
)),
id(new PhabricatorConduitEditField())
->setKey('paths.set')
->setLabel(pht('Paths'))
->setIsFormField(false)
->setTransactionType(
PhabricatorOwnersPackagePathsTransaction::TRANSACTIONTYPE)
->setConduitDescription(
pht('Overwrite existing package paths with new paths.'))
->setConduitTypeDescription(
pht('List of dictionaries, each describing a path.'))
->setConduitDocumentation($paths_help),
);
}
}
diff --git a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php
index 6d6ccb2ed..67b4836a5 100644
--- a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php
+++ b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php
@@ -1,452 +1,451 @@
<?php
final class PhabricatorOwnersPackageQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $ownerPHIDs;
private $authorityPHIDs;
private $repositoryPHIDs;
private $paths;
private $statuses;
private $controlMap = array();
private $controlResults;
private $needPaths;
/**
* Query owner PHIDs exactly. This does not expand authorities, so a user
* PHID will not match projects the user is a member of.
*/
public function withOwnerPHIDs(array $phids) {
$this->ownerPHIDs = $phids;
return $this;
}
/**
* Query owner authority. This will expand authorities, so a user PHID will
* match both packages they own directly and packages owned by a project they
* are a member of.
*/
public function withAuthorityPHIDs(array $phids) {
$this->authorityPHIDs = $phids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withRepositoryPHIDs(array $phids) {
$this->repositoryPHIDs = $phids;
return $this;
}
public function withPaths(array $paths) {
$this->paths = $paths;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withControl($repository_phid, array $paths) {
if (empty($this->controlMap[$repository_phid])) {
$this->controlMap[$repository_phid] = array();
}
foreach ($paths as $path) {
$path = (string)$path;
$this->controlMap[$repository_phid][$path] = $path;
}
// We need to load paths to execute control queries.
$this->needPaths = true;
return $this;
}
public function withNameNgrams($ngrams) {
return $this->withNgramsConstraint(
new PhabricatorOwnersPackageNameNgrams(),
$ngrams);
}
public function needPaths($need_paths) {
$this->needPaths = $need_paths;
return $this;
}
public function newResultObject() {
return new PhabricatorOwnersPackage();
}
protected function willExecute() {
$this->controlResults = array();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $packages) {
$package_ids = mpull($packages, 'getID');
$owners = id(new PhabricatorOwnersOwner())->loadAllWhere(
'packageID IN (%Ld)',
$package_ids);
$owners = mgroup($owners, 'getPackageID');
foreach ($packages as $package) {
$package->attachOwners(idx($owners, $package->getID(), array()));
}
return $packages;
}
protected function didFilterPage(array $packages) {
$package_ids = mpull($packages, 'getID');
if ($this->needPaths) {
$paths = id(new PhabricatorOwnersPath())->loadAllWhere(
'packageID IN (%Ld)',
$package_ids);
$paths = mgroup($paths, 'getPackageID');
foreach ($packages as $package) {
$package->attachPaths(idx($paths, $package->getID(), array()));
}
}
if ($this->controlMap) {
foreach ($packages as $package) {
// If this package is archived, it's no longer a controlling package
// for any path. In particular, it can not force active packages with
// weak dominion to give up control.
if ($package->isArchived()) {
continue;
}
$this->controlResults[$package->getID()] = $package;
}
}
return $packages;
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->shouldJoinOwnersTable()) {
$joins[] = qsprintf(
$conn,
'JOIN %T o ON o.packageID = p.id',
id(new PhabricatorOwnersOwner())->getTableName());
}
if ($this->shouldJoinPathTable()) {
$joins[] = qsprintf(
$conn,
'JOIN %T rpath ON rpath.packageID = p.id',
id(new PhabricatorOwnersPath())->getTableName());
}
return $joins;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'p.phid IN (%Ls)',
$this->phids);
}
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'p.id IN (%Ld)',
$this->ids);
}
if ($this->repositoryPHIDs !== null) {
$where[] = qsprintf(
$conn,
'rpath.repositoryPHID IN (%Ls)',
$this->repositoryPHIDs);
}
if ($this->authorityPHIDs !== null) {
$authority_phids = $this->expandAuthority($this->authorityPHIDs);
$where[] = qsprintf(
$conn,
'o.userPHID IN (%Ls)',
$authority_phids);
}
if ($this->ownerPHIDs !== null) {
$where[] = qsprintf(
$conn,
'o.userPHID IN (%Ls)',
$this->ownerPHIDs);
}
if ($this->paths !== null) {
$where[] = qsprintf(
$conn,
'rpath.pathIndex IN (%Ls)',
$this->getFragmentIndexesForPaths($this->paths));
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'p.status IN (%Ls)',
$this->statuses);
}
if ($this->controlMap) {
$clauses = array();
foreach ($this->controlMap as $repository_phid => $paths) {
$indexes = $this->getFragmentIndexesForPaths($paths);
$clauses[] = qsprintf(
$conn,
'(rpath.repositoryPHID = %s AND rpath.pathIndex IN (%Ls))',
$repository_phid,
$indexes);
}
$where[] = qsprintf($conn, '%LO', $clauses);
}
return $where;
}
protected function shouldGroupQueryResultRows() {
if ($this->shouldJoinOwnersTable()) {
return true;
}
if ($this->shouldJoinPathTable()) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Name'),
),
) + parent::getBuiltinOrders();
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'name',
'type' => 'string',
'unique' => true,
'reverse' => true,
),
);
}
- protected function getPagingValueMap($cursor, array $keys) {
- $package = $this->loadCursorObject($cursor);
+ protected function newPagingMapFromPartialObject($object) {
return array(
- 'id' => $package->getID(),
- 'name' => $package->getName(),
+ 'id' => (int)$object->getID(),
+ 'name' => $object->getName(),
);
}
public function getQueryApplicationClass() {
return 'PhabricatorOwnersApplication';
}
protected function getPrimaryTableAlias() {
return 'p';
}
private function shouldJoinOwnersTable() {
if ($this->ownerPHIDs !== null) {
return true;
}
if ($this->authorityPHIDs !== null) {
return true;
}
return false;
}
private function shouldJoinPathTable() {
if ($this->repositoryPHIDs !== null) {
return true;
}
if ($this->paths !== null) {
return true;
}
if ($this->controlMap) {
return true;
}
return false;
}
private function expandAuthority(array $phids) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withMemberPHIDs($phids)
->execute();
$project_phids = mpull($projects, 'getPHID');
return array_fuse($phids) + array_fuse($project_phids);
}
private function getFragmentsForPaths(array $paths) {
$fragments = array();
foreach ($paths as $path) {
foreach (PhabricatorOwnersPackage::splitPath($path) as $fragment) {
$fragments[$fragment] = $fragment;
}
}
return $fragments;
}
private function getFragmentIndexesForPaths(array $paths) {
$indexes = array();
foreach ($this->getFragmentsForPaths($paths) as $fragment) {
$indexes[] = PhabricatorHash::digestForIndex($fragment);
}
return $indexes;
}
/* -( Path Control )------------------------------------------------------- */
/**
* Get a list of all packages which control a path or its parent directories,
* ordered from weakest to strongest.
*
* The first package has the most specific claim on the path; the last
* package has the most general claim. Multiple packages may have claims of
* equal strength, so this ordering is primarily one of usability and
* convenience.
*
* @return list<PhabricatorOwnersPackage> List of controlling packages.
*/
public function getControllingPackagesForPath(
$repository_phid,
$path,
$ignore_dominion = false) {
$path = (string)$path;
if (!isset($this->controlMap[$repository_phid][$path])) {
throw new PhutilInvalidStateException('withControl');
}
if ($this->controlResults === null) {
throw new PhutilInvalidStateException('execute');
}
$packages = $this->controlResults;
$weak_dominion = PhabricatorOwnersPackage::DOMINION_WEAK;
$path_fragments = PhabricatorOwnersPackage::splitPath($path);
$fragment_count = count($path_fragments);
$matches = array();
foreach ($packages as $package_id => $package) {
$best_match = null;
$include = false;
$repository_paths = $package->getPathsForRepository($repository_phid);
foreach ($repository_paths as $package_path) {
$strength = $package_path->getPathMatchStrength(
$path_fragments,
$fragment_count);
if ($strength > $best_match) {
$best_match = $strength;
$include = !$package_path->getExcluded();
}
}
if ($best_match && $include) {
if ($ignore_dominion) {
$is_weak = false;
} else {
$is_weak = ($package->getDominion() == $weak_dominion);
}
$matches[$package_id] = array(
'strength' => $best_match,
'weak' => $is_weak,
'package' => $package,
);
}
}
// At each strength level, drop weak packages if there are also strong
// packages of the same strength.
$strength_map = igroup($matches, 'strength');
foreach ($strength_map as $strength => $package_list) {
$any_strong = false;
foreach ($package_list as $package_id => $package) {
if (!$package['weak']) {
$any_strong = true;
break;
}
}
if ($any_strong) {
foreach ($package_list as $package_id => $package) {
if ($package['weak']) {
unset($matches[$package_id]);
}
}
}
}
$matches = isort($matches, 'strength');
$matches = array_reverse($matches);
$strongest = null;
foreach ($matches as $package_id => $match) {
if ($strongest === null) {
$strongest = $match['strength'];
}
if ($match['strength'] === $strongest) {
continue;
}
if ($match['weak']) {
unset($matches[$package_id]);
}
}
return array_values(ipull($matches, 'package'));
}
}
diff --git a/src/applications/owners/storage/PhabricatorOwnersPackage.php b/src/applications/owners/storage/PhabricatorOwnersPackage.php
index 564fc8a28..b9e91ef95 100644
--- a/src/applications/owners/storage/PhabricatorOwnersPackage.php
+++ b/src/applications/owners/storage/PhabricatorOwnersPackage.php
@@ -1,805 +1,803 @@
<?php
final class PhabricatorOwnersPackage
extends PhabricatorOwnersDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface,
PhabricatorConduitResultInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorNgramsInterface {
protected $name;
- protected $auditingEnabled;
protected $autoReview;
protected $description;
protected $status;
protected $viewPolicy;
protected $editPolicy;
protected $dominion;
protected $properties = array();
+ protected $auditingState;
private $paths = self::ATTACHABLE;
private $owners = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $pathRepositoryMap = array();
const STATUS_ACTIVE = 'active';
const STATUS_ARCHIVED = 'archived';
const AUTOREVIEW_NONE = 'none';
const AUTOREVIEW_SUBSCRIBE = 'subscribe';
const AUTOREVIEW_SUBSCRIBE_ALWAYS = 'subscribe-always';
const AUTOREVIEW_REVIEW = 'review';
const AUTOREVIEW_REVIEW_ALWAYS = 'review-always';
const AUTOREVIEW_BLOCK = 'block';
const AUTOREVIEW_BLOCK_ALWAYS = 'block-always';
const DOMINION_STRONG = 'strong';
const DOMINION_WEAK = 'weak';
const PROPERTY_IGNORED = 'ignored';
public static function initializeNewPackage(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorOwnersApplication'))
->executeOne();
$view_policy = $app->getPolicy(
PhabricatorOwnersDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(
PhabricatorOwnersDefaultEditCapability::CAPABILITY);
return id(new PhabricatorOwnersPackage())
- ->setAuditingEnabled(0)
+ ->setAuditingState(PhabricatorOwnersAuditRule::AUDITING_NONE)
->setAutoReview(self::AUTOREVIEW_NONE)
->setDominion(self::DOMINION_STRONG)
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->attachPaths(array())
->setStatus(self::STATUS_ACTIVE)
->attachOwners(array())
->setDescription('');
}
public static function getStatusNameMap() {
return array(
self::STATUS_ACTIVE => pht('Active'),
self::STATUS_ARCHIVED => pht('Archived'),
);
}
public static function getAutoreviewOptionsMap() {
return array(
self::AUTOREVIEW_NONE => array(
'name' => pht('No Autoreview'),
),
self::AUTOREVIEW_REVIEW => array(
'name' => pht('Review Changes With Non-Owner Author'),
'authority' => true,
),
self::AUTOREVIEW_BLOCK => array(
'name' => pht('Review Changes With Non-Owner Author (Blocking)'),
'authority' => true,
),
self::AUTOREVIEW_SUBSCRIBE => array(
'name' => pht('Subscribe to Changes With Non-Owner Author'),
'authority' => true,
),
self::AUTOREVIEW_REVIEW_ALWAYS => array(
'name' => pht('Review All Changes'),
),
self::AUTOREVIEW_BLOCK_ALWAYS => array(
'name' => pht('Review All Changes (Blocking)'),
),
self::AUTOREVIEW_SUBSCRIBE_ALWAYS => array(
'name' => pht('Subscribe to All Changes'),
),
);
}
public static function getDominionOptionsMap() {
return array(
self::DOMINION_STRONG => array(
'name' => pht('Strong (Control All Paths)'),
'short' => pht('Strong'),
),
self::DOMINION_WEAK => array(
'name' => pht('Weak (Control Unowned Paths)'),
'short' => pht('Weak'),
),
);
}
protected function getConfiguration() {
return array(
// This information is better available from the history table.
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort',
'description' => 'text',
- 'auditingEnabled' => 'bool',
+ 'auditingState' => 'text32',
'status' => 'text32',
'autoReview' => 'text32',
'dominion' => 'text32',
),
) + parent::getConfiguration();
}
public function getPHIDType() {
return PhabricatorOwnersPackagePHIDType::TYPECONST;
}
public function isArchived() {
return ($this->getStatus() == self::STATUS_ARCHIVED);
}
public function getMustMatchUngeneratedPaths() {
$ignore_attributes = $this->getIgnoredPathAttributes();
return !empty($ignore_attributes['generated']);
}
public function getPackageProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setPackageProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getIgnoredPathAttributes() {
return $this->getPackageProperty(self::PROPERTY_IGNORED, array());
}
public function setIgnoredPathAttributes(array $attributes) {
return $this->setPackageProperty(self::PROPERTY_IGNORED, $attributes);
}
public function loadOwners() {
if (!$this->getID()) {
return array();
}
return id(new PhabricatorOwnersOwner())->loadAllWhere(
'packageID = %d',
$this->getID());
}
public function loadPaths() {
if (!$this->getID()) {
return array();
}
return id(new PhabricatorOwnersPath())->loadAllWhere(
'packageID = %d',
$this->getID());
}
public static function loadAffectedPackages(
PhabricatorRepository $repository,
array $paths) {
if (!$paths) {
return array();
}
return self::loadPackagesForPaths($repository, $paths);
}
public static function loadAffectedPackagesForChangesets(
PhabricatorRepository $repository,
DifferentialDiff $diff,
array $changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
$paths_all = array();
$paths_ungenerated = array();
foreach ($changesets as $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $diff);
$paths_all[] = $path;
if (!$changeset->isGeneratedChangeset()) {
$paths_ungenerated[] = $path;
}
}
if (!$paths_all) {
return array();
}
$packages_all = self::loadAffectedPackages(
$repository,
$paths_all);
// If there are no generated changesets, we can't possibly need to throw
// away any packages for matching only generated paths. Just return the
// full set of packages.
if ($paths_ungenerated === $paths_all) {
return $packages_all;
}
$must_match_ungenerated = array();
foreach ($packages_all as $package) {
if ($package->getMustMatchUngeneratedPaths()) {
$must_match_ungenerated[] = $package;
}
}
// If no affected packages have the "Ignore Generated Paths" flag set, we
// can't possibly need to throw any away.
if (!$must_match_ungenerated) {
return $packages_all;
}
if ($paths_ungenerated) {
$packages_ungenerated = self::loadAffectedPackages(
$repository,
$paths_ungenerated);
} else {
$packages_ungenerated = array();
}
// We have some generated paths, and some packages that ignore generated
// paths. Take all the packages which:
//
// - ignore generated paths; and
// - didn't match any ungenerated paths
//
// ...and remove them from the list.
$must_match_ungenerated = mpull($must_match_ungenerated, null, 'getID');
$packages_ungenerated = mpull($packages_ungenerated, null, 'getID');
$packages_all = mpull($packages_all, null, 'getID');
foreach ($must_match_ungenerated as $package_id => $package) {
if (!isset($packages_ungenerated[$package_id])) {
unset($packages_all[$package_id]);
}
}
return $packages_all;
}
public static function loadOwningPackages($repository, $path) {
if (empty($path)) {
return array();
}
return self::loadPackagesForPaths($repository, array($path), 1);
}
private static function loadPackagesForPaths(
PhabricatorRepository $repository,
array $paths,
$limit = 0) {
$fragments = array();
foreach ($paths as $path) {
foreach (self::splitPath($path) as $fragment) {
$fragments[$fragment][$path] = true;
}
}
$package = new PhabricatorOwnersPackage();
$path = new PhabricatorOwnersPath();
$conn = $package->establishConnection('r');
$repository_clause = qsprintf(
$conn,
'AND p.repositoryPHID = %s',
$repository->getPHID());
// NOTE: The list of $paths may be very large if we're coming from
// the OwnersWorker and processing, e.g., an SVN commit which created a new
// branch. Break it apart so that it will fit within 'max_allowed_packet',
// and then merge results in PHP.
$rows = array();
foreach (array_chunk(array_keys($fragments), 1024) as $chunk) {
$indexes = array();
foreach ($chunk as $fragment) {
$indexes[] = PhabricatorHash::digestForIndex($fragment);
}
$rows[] = queryfx_all(
$conn,
'SELECT pkg.id, pkg.dominion, p.excluded, p.path
FROM %T pkg JOIN %T p ON p.packageID = pkg.id
WHERE p.pathIndex IN (%Ls) AND pkg.status IN (%Ls) %Q',
$package->getTableName(),
$path->getTableName(),
$indexes,
array(
self::STATUS_ACTIVE,
),
$repository_clause);
}
$rows = array_mergev($rows);
$ids = self::findLongestPathsPerPackage($rows, $fragments);
if (!$ids) {
return array();
}
arsort($ids);
if ($limit) {
$ids = array_slice($ids, 0, $limit, $preserve_keys = true);
}
$ids = array_keys($ids);
$packages = $package->loadAllWhere('id in (%Ld)', $ids);
$packages = array_select_keys($packages, $ids);
return $packages;
}
public static function loadPackagesForRepository($repository) {
$package = new PhabricatorOwnersPackage();
$ids = ipull(
queryfx_all(
$package->establishConnection('r'),
'SELECT DISTINCT packageID FROM %T WHERE repositoryPHID = %s',
id(new PhabricatorOwnersPath())->getTableName(),
$repository->getPHID()),
'packageID');
return $package->loadAllWhere('id in (%Ld)', $ids);
}
public static function findLongestPathsPerPackage(array $rows, array $paths) {
// Build a map from each path to all the package paths which match it.
$path_hits = array();
$weak = array();
foreach ($rows as $row) {
$id = $row['id'];
$path = $row['path'];
$length = strlen($path);
$excluded = $row['excluded'];
if ($row['dominion'] === self::DOMINION_WEAK) {
$weak[$id] = true;
}
$matches = $paths[$path];
foreach ($matches as $match => $ignored) {
$path_hits[$match][] = array(
'id' => $id,
'excluded' => $excluded,
'length' => $length,
);
}
}
// For each path, process the matching package paths to figure out which
// packages actually own it.
$path_packages = array();
foreach ($path_hits as $match => $hits) {
$hits = isort($hits, 'length');
$packages = array();
foreach ($hits as $hit) {
$package_id = $hit['id'];
if ($hit['excluded']) {
unset($packages[$package_id]);
} else {
$packages[$package_id] = $hit;
}
}
$path_packages[$match] = $packages;
}
// Remove packages with weak dominion rules that should cede control to
// a more specific package.
if ($weak) {
foreach ($path_packages as $match => $packages) {
// Group packages by length.
$length_map = array();
foreach ($packages as $package_id => $package) {
$length_map[$package['length']][$package_id] = $package;
}
// For each path length, remove all weak packages if there are any
// strong packages of the same length. This makes sure that if there
// are one or more strong claims on a particular path, only those
// claims stand.
foreach ($length_map as $package_list) {
$any_strong = false;
foreach ($package_list as $package_id => $package) {
if (!isset($weak[$package_id])) {
$any_strong = true;
break;
}
}
if ($any_strong) {
foreach ($package_list as $package_id => $package) {
if (isset($weak[$package_id])) {
unset($packages[$package_id]);
}
}
}
}
$packages = isort($packages, 'length');
$packages = array_reverse($packages, true);
$best_length = null;
foreach ($packages as $package_id => $package) {
// If this is the first package we've encountered, note its length.
// We're iterating over the packages from longest to shortest match,
// so packages of this length always have the best claim on the path.
if ($best_length === null) {
$best_length = $package['length'];
}
// If this package has the same length as the best length, its claim
// stands.
if ($package['length'] === $best_length) {
continue;
}
// If this is a weak package and does not have the best length,
// cede its claim to the stronger package.
if (isset($weak[$package_id])) {
unset($packages[$package_id]);
}
}
$path_packages[$match] = $packages;
}
}
// For each package that owns at least one path, identify the longest
// path it owns.
$package_lengths = array();
foreach ($path_packages as $match => $hits) {
foreach ($hits as $hit) {
$length = $hit['length'];
$id = $hit['id'];
if (empty($package_lengths[$id])) {
$package_lengths[$id] = $length;
} else {
$package_lengths[$id] = max($package_lengths[$id], $length);
}
}
}
return $package_lengths;
}
public static function splitPath($path) {
$result = array(
'/',
);
$parts = explode('/', $path);
$buffer = '/';
foreach ($parts as $part) {
if (!strlen($part)) {
continue;
}
$buffer = $buffer.$part.'/';
$result[] = $buffer;
}
return $result;
}
public function attachPaths(array $paths) {
assert_instances_of($paths, 'PhabricatorOwnersPath');
$this->paths = $paths;
// Drop this cache if we're attaching new paths.
$this->pathRepositoryMap = array();
return $this;
}
public function getPaths() {
return $this->assertAttached($this->paths);
}
public function getPathsForRepository($repository_phid) {
if (isset($this->pathRepositoryMap[$repository_phid])) {
return $this->pathRepositoryMap[$repository_phid];
}
$map = array();
foreach ($this->getPaths() as $path) {
if ($path->getRepositoryPHID() == $repository_phid) {
$map[] = $path;
}
}
$this->pathRepositoryMap[$repository_phid] = $map;
return $this->pathRepositoryMap[$repository_phid];
}
public function attachOwners(array $owners) {
assert_instances_of($owners, 'PhabricatorOwnersOwner');
$this->owners = $owners;
return $this;
}
public function getOwners() {
return $this->assertAttached($this->owners);
}
public function getOwnerPHIDs() {
return mpull($this->getOwners(), 'getUserPHID');
}
public function isOwnerPHID($phid) {
if (!$phid) {
return false;
}
$owner_phids = $this->getOwnerPHIDs();
$owner_phids = array_fuse($owner_phids);
return isset($owner_phids[$phid]);
}
public function getMonogram() {
return 'O'.$this->getID();
}
public function getURI() {
// TODO: Move these to "/O123" for consistency.
return '/owners/package/'.$this->getID().'/';
}
+ public function newAuditingRule() {
+ return PhabricatorOwnersAuditRule::newFromState($this->getAuditingState());
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if ($this->isOwnerPHID($viewer->getPHID())) {
return true;
}
break;
}
return false;
}
public function describeAutomaticCapability($capability) {
return pht('Owners of a package may always view it.');
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorOwnersPackageTransactionEditor();
}
public function getApplicationTransactionTemplate() {
return new PhabricatorOwnersPackageTransaction();
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('owners.fields');
}
public function getCustomFieldBaseClass() {
return 'PhabricatorOwnersCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$conn_w = $this->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE packageID = %d',
id(new PhabricatorOwnersPath())->getTableName(),
$this->getID());
queryfx(
$conn_w,
'DELETE FROM %T WHERE packageID = %d',
id(new PhabricatorOwnersOwner())->getTableName(),
$this->getID());
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the package.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('description')
->setType('string')
->setDescription(pht('The package description.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('status')
->setType('string')
->setDescription(pht('Active or archived status of the package.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('owners')
->setType('list<map<string, wild>>')
->setDescription(pht('List of package owners.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('review')
->setType('map<string, wild>')
->setDescription(pht('Auto review information.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('audit')
->setType('map<string, wild>')
->setDescription(pht('Auto audit information.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('dominion')
->setType('map<string, wild>')
->setDescription(pht('Dominion setting information.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('ignored')
->setType('map<string, wild>')
->setDescription(pht('Ignored attribute information.')),
);
}
public function getFieldValuesForConduit() {
$owner_list = array();
foreach ($this->getOwners() as $owner) {
$owner_list[] = array(
'ownerPHID' => $owner->getUserPHID(),
);
}
$review_map = self::getAutoreviewOptionsMap();
$review_value = $this->getAutoReview();
if (isset($review_map[$review_value])) {
$review_label = $review_map[$review_value]['name'];
} else {
$review_label = pht('Unknown ("%s")', $review_value);
}
$review = array(
'value' => $review_value,
'label' => $review_label,
);
- if ($this->getAuditingEnabled()) {
- $audit_value = 'audit';
- $audit_label = pht('Auditing Enabled');
- } else {
- $audit_value = 'none';
- $audit_label = pht('No Auditing');
- }
+ $audit_rule = $this->newAuditingRule();
$audit = array(
- 'value' => $audit_value,
- 'label' => $audit_label,
+ 'value' => $audit_rule->getKey(),
+ 'label' => $audit_rule->getDisplayName(),
);
$dominion_value = $this->getDominion();
$dominion_map = self::getDominionOptionsMap();
if (isset($dominion_map[$dominion_value])) {
$dominion_label = $dominion_map[$dominion_value]['name'];
$dominion_short = $dominion_map[$dominion_value]['short'];
} else {
$dominion_label = pht('Unknown ("%s")', $dominion_value);
$dominion_short = pht('Unknown ("%s")', $dominion_value);
}
$dominion = array(
'value' => $dominion_value,
'label' => $dominion_label,
'short' => $dominion_short,
);
// Force this to always emit as a JSON object even if empty, never as
// a JSON list.
$ignored = $this->getIgnoredPathAttributes();
if (!$ignored) {
$ignored = (object)array();
}
return array(
'name' => $this->getName(),
'description' => $this->getDescription(),
'status' => $this->getStatus(),
'owners' => $owner_list,
'review' => $review,
'audit' => $audit,
'dominion' => $dominion,
'ignored' => $ignored,
);
}
public function getConduitSearchAttachments() {
return array(
id(new PhabricatorOwnersPathsSearchEngineAttachment())
->setAttachmentKey('paths'),
);
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhabricatorOwnersPackageFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new PhabricatorOwnersPackageFerretEngine();
}
/* -( PhabricatorNgramsInterface )----------------------------------------- */
public function newNgrams() {
return array(
id(new PhabricatorOwnersPackageNameNgrams())
->setValue($this->getName()),
);
}
}
diff --git a/src/applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php b/src/applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php
index df4f0feb0..7c16c850f 100644
--- a/src/applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php
+++ b/src/applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php
@@ -1,32 +1,67 @@
<?php
final class PhabricatorOwnersPackageAuditingTransaction
extends PhabricatorOwnersPackageTransactionType {
const TRANSACTIONTYPE = 'owners.auditing';
public function generateOldValue($object) {
- return (int)$object->getAuditingEnabled();
+ return $object->getAuditingState();
}
public function generateNewValue($object, $value) {
- return (int)$value;
+ return PhabricatorOwnersAuditRule::getStorageValueFromAPIValue($value);
}
public function applyInternalEffects($object, $value) {
- $object->setAuditingEnabled($value);
+ $object->setAuditingState($value);
}
public function getTitle() {
- if ($this->getNewValue()) {
- return pht(
- '%s enabled auditing for this package.',
- $this->renderAuthor());
- } else {
- return pht(
- '%s disabled auditing for this package.',
- $this->renderAuthor());
+ $old_value = $this->getOldValue();
+ $new_value = $this->getNewValue();
+
+ $old_rule = PhabricatorOwnersAuditRule::newFromState($old_value);
+ $new_rule = PhabricatorOwnersAuditRule::newFromState($new_value);
+
+ return pht(
+ '%s changed the audit rule for this package from %s to %s.',
+ $this->renderAuthor(),
+ $this->renderValue($old_rule->getDisplayName()),
+ $this->renderValue($new_rule->getDisplayName()));
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+
+ // See PHI1047. This transaction type accepted some weird stuff. Continue
+ // supporting it for now, but move toward sensible consistency.
+
+ $modern_options = PhabricatorOwnersAuditRule::getModernValueMap();
+ $deprecated_options = PhabricatorOwnersAuditRule::getDeprecatedValueMap();
+
+ foreach ($xactions as $xaction) {
+ $new_value = $xaction->getNewValue();
+
+ if (isset($modern_options[$new_value])) {
+ continue;
+ }
+
+ if (isset($deprecated_options[$new_value])) {
+ continue;
+ }
+
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Package auditing value "%s" is not supported. Supported options '.
+ 'are: %s. Deprecated options are: %s.',
+ $new_value,
+ implode(', ', $modern_options),
+ implode(', ', $deprecated_options)),
+ $xaction);
}
+
+ return $errors;
}
}
diff --git a/src/applications/passphrase/storage/PassphraseCredentialTransaction.php b/src/applications/passphrase/storage/PassphraseCredentialTransaction.php
index b7e4f904e..bbc3b0966 100644
--- a/src/applications/passphrase/storage/PassphraseCredentialTransaction.php
+++ b/src/applications/passphrase/storage/PassphraseCredentialTransaction.php
@@ -1,22 +1,18 @@
<?php
final class PassphraseCredentialTransaction
extends PhabricatorModularTransaction {
public function getApplicationName() {
return 'passphrase';
}
public function getApplicationTransactionType() {
return PassphraseCredentialPHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return null;
- }
-
public function getBaseTransactionClass() {
return 'PassphraseCredentialTransactionType';
}
}
diff --git a/src/applications/people/application/PhabricatorPeopleApplication.php b/src/applications/people/application/PhabricatorPeopleApplication.php
index 9238d8da3..9cc360793 100644
--- a/src/applications/people/application/PhabricatorPeopleApplication.php
+++ b/src/applications/people/application/PhabricatorPeopleApplication.php
@@ -1,113 +1,113 @@
<?php
final class PhabricatorPeopleApplication extends PhabricatorApplication {
public function getName() {
return pht('People');
}
public function getShortDescription() {
return pht('User Accounts and Profiles');
}
public function getBaseURI() {
return '/people/';
}
public function getTitleGlyph() {
return "\xE2\x99\x9F";
}
public function getIcon() {
return 'fa-users';
}
public function isPinnedByDefault(PhabricatorUser $viewer) {
return $viewer->getIsAdmin();
}
public function getFlavorText() {
return pht('Sort of a social utility.');
}
public function getApplicationGroup() {
return self::GROUP_UTILITIES;
}
public function canUninstall() {
return false;
}
public function getRoutes() {
return array(
'/people/' => array(
$this->getQueryRoutePattern() => 'PhabricatorPeopleListController',
'logs/' => array(
$this->getQueryRoutePattern() => 'PhabricatorPeopleLogsController',
),
'invite/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhabricatorPeopleInviteListController',
'send/'
=> 'PhabricatorPeopleInviteSendController',
),
- 'approve/(?P<id>[1-9]\d*)/' => 'PhabricatorPeopleApproveController',
+ 'approve/(?P<id>[1-9]\d*)/(?:via/(?P<via>[^/]+)/)?'
+ => 'PhabricatorPeopleApproveController',
'(?P<via>disapprove)/(?P<id>[1-9]\d*)/'
=> 'PhabricatorPeopleDisableController',
'(?P<via>disable)/(?P<id>[1-9]\d*)/'
=> 'PhabricatorPeopleDisableController',
'empower/(?P<id>[1-9]\d*)/' => 'PhabricatorPeopleEmpowerController',
'delete/(?P<id>[1-9]\d*)/' => 'PhabricatorPeopleDeleteController',
'rename/(?P<id>[1-9]\d*)/' => 'PhabricatorPeopleRenameController',
'welcome/(?P<id>[1-9]\d*)/' => 'PhabricatorPeopleWelcomeController',
'create/' => 'PhabricatorPeopleCreateController',
'new/(?P<type>[^/]+)/' => 'PhabricatorPeopleNewController',
- 'ldap/' => 'PhabricatorPeopleLdapController',
'editprofile/(?P<id>[1-9]\d*)/' =>
'PhabricatorPeopleProfileEditController',
'badges/(?P<id>[1-9]\d*)/' =>
'PhabricatorPeopleProfileBadgesController',
'tasks/(?P<id>[1-9]\d*)/' =>
'PhabricatorPeopleProfileTasksController',
'commits/(?P<id>[1-9]\d*)/' =>
'PhabricatorPeopleProfileCommitsController',
'revisions/(?P<id>[1-9]\d*)/' =>
'PhabricatorPeopleProfileRevisionsController',
'picture/(?P<id>[1-9]\d*)/' =>
'PhabricatorPeopleProfilePictureController',
'manage/(?P<id>[1-9]\d*)/' =>
'PhabricatorPeopleProfileManageController',
),
'/p/(?P<username>[\w._-]+)/' => array(
'' => 'PhabricatorPeopleProfileViewController',
'item/' => $this->getProfileMenuRouting(
'PhabricatorPeopleProfileMenuController'),
),
);
}
public function getRemarkupRules() {
return array(
new PhabricatorMentionRemarkupRule(),
);
}
protected function getCustomCapabilities() {
return array(
PeopleCreateUsersCapability::CAPABILITY => array(
'default' => PhabricatorPolicies::POLICY_ADMIN,
),
PeopleDisableUsersCapability::CAPABILITY => array(
'default' => PhabricatorPolicies::POLICY_ADMIN,
),
PeopleBrowseUserDirectoryCapability::CAPABILITY => array(),
);
}
public function getApplicationSearchDocumentTypes() {
return array(
PhabricatorPeopleUserPHIDType::TYPECONST,
);
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleApproveController.php b/src/applications/people/controller/PhabricatorPeopleApproveController.php
index 013f4371f..62594243f 100644
--- a/src/applications/people/controller/PhabricatorPeopleApproveController.php
+++ b/src/applications/people/controller/PhabricatorPeopleApproveController.php
@@ -1,52 +1,67 @@
<?php
final class PhabricatorPeopleApproveController
extends PhabricatorPeopleController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->executeOne();
if (!$user) {
return new Aphront404Response();
}
- $done_uri = $this->getApplicationURI('query/approval/');
+ $via = $request->getURIData('via');
+ switch ($via) {
+ case 'profile':
+ $done_uri = urisprintf('/people/manage/%d/', $user->getID());
+ break;
+ default:
+ $done_uri = $this->getApplicationURI('query/approval/');
+ break;
+ }
+
+ if ($user->getIsApproved()) {
+ return $this->newDialog()
+ ->setTitle(pht('Already Approved'))
+ ->appendChild(pht('This user has already been approved.'))
+ ->addCancelButton($done_uri);
+ }
if ($user->getIsApproved()) {
return $this->newDialog()
->setTitle(pht('Already Approved'))
->appendChild(pht('This user has already been approved.'))
->addCancelButton($done_uri);
}
if ($request->isFormPost()) {
$xactions = array();
$xactions[] = id(new PhabricatorUserTransaction())
->setTransactionType(PhabricatorUserApproveTransaction::TRANSACTIONTYPE)
->setNewValue(true);
id(new PhabricatorUserTransactionEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true)
->applyTransactions($user, $xactions);
return id(new AphrontRedirectResponse())->setURI($done_uri);
}
return $this->newDialog()
->setTitle(pht('Confirm Approval'))
->appendChild(
pht(
'Allow %s to access this Phabricator install?',
phutil_tag('strong', array(), $user->getUsername())))
->addCancelButton($done_uri)
->addSubmitButton(pht('Approve Account'));
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleController.php b/src/applications/people/controller/PhabricatorPeopleController.php
index e3b60eff2..c2c262f9f 100644
--- a/src/applications/people/controller/PhabricatorPeopleController.php
+++ b/src/applications/people/controller/PhabricatorPeopleController.php
@@ -1,47 +1,43 @@
<?php
abstract class PhabricatorPeopleController extends PhabricatorController {
public function shouldRequireAdmin() {
return true;
}
public function buildSideNavView($for_app = false) {
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
$name = null;
if ($for_app) {
$name = $this->getRequest()->getURIData('username');
if ($name) {
$nav->setBaseURI(new PhutilURI('/p/'));
$nav->addFilter("{$name}/", $name);
$nav->addFilter("{$name}/calendar/", pht('Calendar'));
}
}
if (!$name) {
$viewer = $this->getRequest()->getUser();
id(new PhabricatorPeopleSearchEngine())
->setViewer($viewer)
->addNavigationItems($nav->getMenu());
if ($viewer->getIsAdmin()) {
$nav->addLabel(pht('User Administration'));
- if (PhabricatorLDAPAuthProvider::getLDAPProvider()) {
- $nav->addFilter('ldap', pht('Import from LDAP'));
- }
-
$nav->addFilter('logs', pht('Activity Logs'));
$nav->addFilter('invite', pht('Email Invitations'));
}
}
return $nav;
}
public function buildApplicationMenu() {
return $this->buildSideNavView(true)->getMenu();
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleProfileController.php b/src/applications/people/controller/PhabricatorPeopleProfileController.php
index 902b21efc..91afda123 100644
--- a/src/applications/people/controller/PhabricatorPeopleProfileController.php
+++ b/src/applications/people/controller/PhabricatorPeopleProfileController.php
@@ -1,128 +1,141 @@
<?php
abstract class PhabricatorPeopleProfileController
extends PhabricatorPeopleController {
private $user;
private $profileMenu;
public function shouldRequireAdmin() {
return false;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function getUser() {
return $this->user;
}
public function buildApplicationMenu() {
$menu = $this->newApplicationMenu();
$profile_menu = $this->getProfileMenu();
if ($profile_menu) {
$menu->setProfileMenu($profile_menu);
}
return $menu;
}
protected function getProfileMenu() {
if (!$this->profileMenu) {
$user = $this->getUser();
if ($user) {
$viewer = $this->getViewer();
$engine = id(new PhabricatorPeopleProfileMenuEngine())
->setViewer($viewer)
->setProfileObject($user);
$this->profileMenu = $engine->buildNavigation();
}
}
return $this->profileMenu;
}
protected function buildApplicationCrumbs() {
$crumbs = parent::buildApplicationCrumbs();
$user = $this->getUser();
if ($user) {
$crumbs->addTextCrumb(
$user->getUsername(),
urisprintf('/p/%s/', $user->getUsername()));
}
return $crumbs;
}
public function buildProfileHeader() {
$user = $this->user;
$viewer = $this->getViewer();
$profile = $user->loadUserProfile();
$picture = $user->getProfileImageURI();
$profile_icon = PhabricatorPeopleIconSet::getIconIcon($profile->getIcon());
$profile_title = $profile->getDisplayTitle();
- $roles = array();
+
+ $tag = id(new PHUITagView())
+ ->setType(PHUITagView::TYPE_SHADE);
+
+ $tags = array();
if ($user->getIsAdmin()) {
- $roles[] = pht('Administrator');
- }
- if ($user->getIsDisabled()) {
- $roles[] = pht('Disabled');
+ $tags[] = id(clone $tag)
+ ->setName(pht('Administrator'))
+ ->setColor('blue');
}
+
+ // "Disabled" gets a stronger status tag below.
+
if (!$user->getIsApproved()) {
- $roles[] = pht('Not Approved');
+ $tags[] = id(clone $tag)
+ ->setName('Not Approved')
+ ->setColor('yellow');
}
+
if ($user->getIsSystemAgent()) {
- $roles[] = pht('Bot');
+ $tags[] = id(clone $tag)
+ ->setName(pht('Bot'))
+ ->setColor('orange');
}
+
if ($user->getIsMailingList()) {
- $roles[] = pht('Mailing List');
- }
- if (!$user->getIsEmailVerified()) {
- $roles[] = pht('Email Not Verified');
+ $tags[] = id(clone $tag)
+ ->setName(pht('Mailing List'))
+ ->setColor('orange');
}
- $tag = null;
- if ($roles) {
- $tag = id(new PHUITagView())
- ->setName(implode(', ', $roles))
- ->addClass('project-view-header-tag')
- ->setType(PHUITagView::TYPE_SHADE);
+ if (!$user->getIsEmailVerified()) {
+ $tags[] = id(clone $tag)
+ ->setName(pht('Email Not Verified'))
+ ->setColor('violet');
}
$header = id(new PHUIHeaderView())
- ->setHeader(array($user->getFullName(), $tag))
+ ->setHeader($user->getFullName())
->setImage($picture)
->setProfileHeader(true)
->addClass('people-profile-header');
+ foreach ($tags as $tag) {
+ $header->addTag($tag);
+ }
+
require_celerity_resource('project-view-css');
if ($user->getIsDisabled()) {
$header->setStatus('fa-ban', 'red', pht('Disabled'));
} else {
$header->setStatus($profile_icon, 'bluegrey', $profile_title);
}
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$user,
PhabricatorPolicyCapability::CAN_EDIT);
if ($can_edit) {
$id = $user->getID();
$header->setImageEditURL($this->getApplicationURI("picture/{$id}/"));
}
return $header;
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleProfileManageController.php b/src/applications/people/controller/PhabricatorPeopleProfileManageController.php
index fd2b63688..269e7da62 100644
--- a/src/applications/people/controller/PhabricatorPeopleProfileManageController.php
+++ b/src/applications/people/controller/PhabricatorPeopleProfileManageController.php
@@ -1,205 +1,207 @@
<?php
final class PhabricatorPeopleProfileManageController
extends PhabricatorPeopleProfileController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withIDs(array($id))
->needProfile(true)
->needProfileImage(true)
->needAvailability(true)
->executeOne();
if (!$user) {
return new Aphront404Response();
}
$this->setUser($user);
$header = $this->buildProfileHeader();
$curtain = $this->buildCurtain($user);
$properties = $this->buildPropertyView($user);
$name = $user->getUsername();
$nav = $this->getProfileMenu();
$nav->selectFilter(PhabricatorPeopleProfileMenuEngine::ITEM_MANAGE);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Manage'));
$crumbs->setBorder(true);
$timeline = $this->buildTransactionTimeline(
$user,
new PhabricatorPeopleTransactionQuery());
$timeline->setShouldTerminate(true);
$manage = id(new PHUITwoColumnView())
->setHeader($header)
->addClass('project-view-home')
->addClass('project-view-people-home')
->setCurtain($curtain)
->addPropertySection(pht('Details'), $properties)
->setMainColumn($timeline);
return $this->newPage()
->setTitle(
array(
pht('Manage User'),
$user->getUsername(),
))
->setNavigation($nav)
->setCrumbs($crumbs)
->appendChild($manage);
}
private function buildPropertyView(PhabricatorUser $user) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($user);
$field_list = PhabricatorCustomField::getObjectFields(
$user,
PhabricatorCustomField::ROLE_VIEW);
$field_list->appendFieldsToPropertyList($user, $viewer, $view);
return $view;
}
private function buildCurtain(PhabricatorUser $user) {
$viewer = $this->getViewer();
$is_self = ($user->getPHID() === $viewer->getPHID());
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$user,
PhabricatorPolicyCapability::CAN_EDIT);
$is_admin = $viewer->getIsAdmin();
$can_admin = ($is_admin && !$is_self);
$has_disable = $this->hasApplicationCapability(
PeopleDisableUsersCapability::CAPABILITY);
$can_disable = ($has_disable && !$is_self);
+ $id = $user->getID();
+
$welcome_engine = id(new PhabricatorPeopleWelcomeMailEngine())
->setSender($viewer)
->setRecipient($user);
$can_welcome = $welcome_engine->canSendMail();
$curtain = $this->newCurtainView($user);
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Profile'))
- ->setHref($this->getApplicationURI('editprofile/'.$user->getID().'/'))
+ ->setHref($this->getApplicationURI('editprofile/'.$id.'/'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-picture-o')
->setName(pht('Edit Profile Picture'))
- ->setHref($this->getApplicationURI('picture/'.$user->getID().'/'))
+ ->setHref($this->getApplicationURI('picture/'.$id.'/'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-wrench')
->setName(pht('Edit Settings'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setHref('/settings/user/'.$user->getUsername().'/'));
if ($user->getIsAdmin()) {
$empower_icon = 'fa-arrow-circle-o-down';
$empower_name = pht('Remove Administrator');
} else {
$empower_icon = 'fa-arrow-circle-o-up';
$empower_name = pht('Make Administrator');
}
$is_admin = $viewer->getIsAdmin();
$is_self = ($user->getPHID() === $viewer->getPHID());
$can_admin = ($is_admin && !$is_self);
// c4science custo
// Only show admin actions to admin user
if ($is_admin) {
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon($empower_icon)
->setName($empower_name)
->setDisabled(!$can_admin)
->setWorkflow(true)
->setHref($this->getApplicationURI('empower/'.$user->getID().'/')));
}
// c4science custo
if ($can_admin) {
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-tag')
->setName(pht('Change Username'))
->setDisabled(!$is_admin)
->setWorkflow(true)
->setHref($this->getApplicationURI('rename/'.$user->getID().'/')));
}
// c4science custo
if ($is_admin) {
if ($user->getIsDisabled()) {
$disable_icon = 'fa-check-circle-o';
$disable_name = pht('Enable User');
} else {
$disable_icon = 'fa-ban';
$disable_name = pht('Disable User');
}
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon($disable_icon)
->setName($disable_name)
->setDisabled(!$can_admin)
->setWorkflow(true)
->setHref($this->getApplicationURI('disable/'.$user->getID().'/')));
}
// c4science custo
if ($is_admin) {
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-times')
->setName(pht('Delete User'))
->setDisabled(!$can_admin)
->setWorkflow(true)
->setHref($this->getApplicationURI('delete/'.$user->getID().'/')));
$can_welcome = ($is_admin && $user->canEstablishWebSessions());
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-envelope')
->setName(pht('Send Welcome Email'))
->setWorkflow(true)
->setDisabled(!$can_welcome)
->setHref($this->getApplicationURI('welcome/'.$user->getID().'/')));
}
return $curtain;
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php b/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php
index 5cab92425..92bb2e0b8 100644
--- a/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php
+++ b/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php
@@ -1,292 +1,289 @@
<?php
final class PhabricatorPeopleProfilePictureController
extends PhabricatorPeopleProfileController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withIDs(array($id))
->needProfileImage(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$user) {
return new Aphront404Response();
}
$this->setUser($user);
$name = $user->getUserName();
$done_uri = '/p/'.$name.'/';
$supported_formats = PhabricatorFile::getTransformableImageFormats();
$e_file = true;
$errors = array();
if ($request->isFormPost()) {
$phid = $request->getStr('phid');
$is_default = false;
if ($phid == PhabricatorPHIDConstants::PHID_VOID) {
$phid = null;
$is_default = true;
} else if ($phid) {
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
} else {
if ($request->getFileExists('picture')) {
$file = PhabricatorFile::newFromPHPUpload(
$_FILES['picture'],
array(
'authorPHID' => $viewer->getPHID(),
'canCDN' => true,
));
} else {
$e_file = pht('Required');
$errors[] = pht(
'You must choose a file when uploading a new profile picture.');
}
}
if (!$errors && !$is_default) {
if (!$file->isTransformableImage()) {
$e_file = pht('Not Supported');
$errors[] = pht(
'This server only supports these image formats: %s.',
implode(', ', $supported_formats));
} else {
$xform = PhabricatorFileTransform::getTransformByKey(
PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE);
$xformed = $xform->executeTransform($file);
}
}
if (!$errors) {
if ($is_default) {
$user->setProfileImagePHID(null);
} else {
$user->setProfileImagePHID($xformed->getPHID());
$xformed->attachToObject($user->getPHID());
}
$user->save();
return id(new AphrontRedirectResponse())->setURI($done_uri);
}
}
$title = pht('Edit Profile Picture');
$form = id(new PHUIFormLayoutView())
->setUser($viewer);
$default_image = $user->getDefaultProfileImagePHID();
if ($default_image) {
$default_image = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($default_image))
->executeOne();
}
if (!$default_image) {
$default_image = PhabricatorFile::loadBuiltin($viewer, 'profile.png');
}
$images = array();
$current = $user->getProfileImagePHID();
$has_current = false;
if ($current) {
$files = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($current))
->execute();
if ($files) {
$file = head($files);
if ($file->isTransformableImage()) {
$has_current = true;
$images[$current] = array(
'uri' => $file->getBestURI(),
'tip' => pht('Current Picture'),
);
}
}
}
$builtins = array(
'user1.png',
'user2.png',
'user3.png',
'user4.png',
'user5.png',
'user6.png',
'user7.png',
'user8.png',
'user9.png',
);
foreach ($builtins as $builtin) {
$file = PhabricatorFile::loadBuiltin($viewer, $builtin);
$images[$file->getPHID()] = array(
'uri' => $file->getBestURI(),
'tip' => pht('Builtin Image'),
);
}
// Try to add external account images for any associated external accounts.
$accounts = id(new PhabricatorExternalAccountQuery())
->setViewer($viewer)
->withUserPHIDs(array($user->getPHID()))
->needImages(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->execute();
foreach ($accounts as $account) {
$file = $account->getProfileImageFile();
if ($account->getProfileImagePHID() != $file->getPHID()) {
// This is a default image, just skip it.
continue;
}
- $provider = PhabricatorAuthProvider::getEnabledProviderByKey(
- $account->getProviderKey());
- if ($provider) {
- $tip = pht('Picture From %s', $provider->getProviderName());
- } else {
- $tip = pht('Picture From External Account');
- }
+ $config = $account->getProviderConfig();
+ $provider = $config->getProvider();
+
+ $tip = pht('Picture From %s', $provider->getProviderName());
if ($file->isTransformableImage()) {
$images[$file->getPHID()] = array(
'uri' => $file->getBestURI(),
'tip' => $tip,
);
}
}
$images[PhabricatorPHIDConstants::PHID_VOID] = array(
'uri' => $default_image->getBestURI(),
'tip' => pht('Default Picture'),
);
require_celerity_resource('people-profile-css');
Javelin::initBehavior('phabricator-tooltips', array());
$buttons = array();
foreach ($images as $phid => $spec) {
$style = null;
if (isset($spec['style'])) {
$style = $spec['style'];
}
$button = javelin_tag(
'button',
array(
'class' => 'button-grey profile-image-button',
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $spec['tip'],
'size' => 300,
),
),
phutil_tag(
'img',
array(
'height' => 50,
'width' => 50,
'src' => $spec['uri'],
)));
$button = array(
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'phid',
'value' => $phid,
)),
$button,
);
$button = phabricator_form(
$viewer,
array(
'class' => 'profile-image-form',
'method' => 'POST',
),
$button);
$buttons[] = $button;
}
if ($has_current) {
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Current Picture'))
->setValue(array_shift($buttons)));
}
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Use Picture'))
->setValue($buttons));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setFormErrors($errors)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setForm($form);
$upload_form = id(new AphrontFormView())
->setUser($viewer)
->setEncType('multipart/form-data')
->appendChild(
id(new AphrontFormFileControl())
->setName('picture')
->setLabel(pht('Upload Picture'))
->setError($e_file)
->setCaption(
pht('Supported formats: %s', implode(', ', $supported_formats))))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($done_uri)
->setValue(pht('Upload Picture')));
$upload_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Upload New Picture'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setForm($upload_form);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Edit Profile Picture'));
$crumbs->setBorder(true);
$nav = $this->getProfileMenu();
$nav->selectFilter(PhabricatorPeopleProfileMenuEngine::ITEM_MANAGE);
$header = $this->buildProfileHeader();
$view = id(new PHUITwoColumnView())
->setHeader($header)
->addClass('project-view-home')
->addClass('project-view-people-home')
->setFooter(array(
$form_box,
$upload_box,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->setNavigation($nav)
->appendChild($view);
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php
index b2741592c..800cb840c 100644
--- a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php
+++ b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php
@@ -1,340 +1,346 @@
<?php
final class PhabricatorPeopleProfileViewController
extends PhabricatorPeopleProfileController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$username = $request->getURIData('username');
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withUsernames(array($username))
->needProfileImage(true)
->needAvailability(true)
->executeOne();
if (!$user) {
return new Aphront404Response();
}
$this->setUser($user);
$header = $this->buildProfileHeader();
$properties = $this->buildPropertyView($user);
$name = $user->getUsername();
$feed = $this->buildPeopleFeed($user, $viewer);
$view_all = id(new PHUIButtonView())
->setTag('a')
->setIcon(
id(new PHUIIconView())
->setIcon('fa-list-ul'))
->setText(pht('View All'))
->setHref('/feed/?userPHIDs='.$user->getPHID());
$feed_header = id(new PHUIHeaderView())
->setHeader(pht('Recent Activity'))
->addActionLink($view_all);
$feed = id(new PHUIObjectBoxView())
->setHeader($feed_header)
->addClass('project-view-feed')
->appendChild($feed);
$projects = $this->buildProjectsView($user);
$repo = $this->buildRepoView($user); // c4science custo
$calendar = $this->buildCalendarDayView($user);
$home = id(new PHUITwoColumnView())
->setHeader($header)
->addClass('project-view-home')
->addClass('project-view-people-home')
->setMainColumn(
array(
$properties,
$feed,
))
->setSideColumn(
array(
$projects,
$repo, // c4science custo
$calendar,
));
$nav = $this->getProfileMenu();
$nav->selectFilter(PhabricatorPeopleProfileMenuEngine::ITEM_PROFILE);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->setBorder(true);
return $this->newPage()
->setTitle($user->getUsername())
->setNavigation($nav)
->setCrumbs($crumbs)
->setPageObjectPHIDs(
array(
$user->getPHID(),
))
->appendChild(
array(
$home,
));
}
// c4science custo
private function buildRepoView(
PhabricatorUser $user) {
$viewer = $this->getViewer();
$repo_transaction = id(new PhabricatorRepositoryTransactionQuery())
->setViewer($viewer)
->withAuthorPHIDs(array($user->getPHID()))
->withTransactionTypes(array(PhabricatorTransactions::TYPE_CREATE))
->execute();
if(!empty($repo_transaction)) {
$repo_phids = mpull($repo_transaction, 'getObjectPHID');
$repo = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withPHIDs($repo_phids)
->setOrder('newest')
->execute();
}
$header = id(new PHUIHeaderView());
if (!empty($repo)) {
$nb = count($repo);
$limit = 5;
$repo = array_slice($repo, 0, $limit);
$list = new PHUIObjectItemListView();
foreach($repo as $r){
$list->addItem(
id(new PHUIObjectItemView())
->setHeader($r->getName())
->addAttribute($r->getDisplayName())
->setDisabled($r->getStatus() == PhabricatorRepository::STATUS_INACTIVE)
->setHref($r->getURI()));
}
$header_text = pht('Repositories (%s)', $nb);
if($nb > $limit) {
$header->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-list-ul')
->setText(pht('View All'))
->setHref('/diffusion/?order=newest&author=' . $user->getUsername() . '#R'));
}
} else {
$header_text = pht('Repositories');
$error = id(new PHUIBoxView())
->addClass('mlb')
->appendChild(pht('User does not have any repository.'));
$list = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NODATA)
->appendChild($error);
}
$header->setHeader($header_text);
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($list)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
return $box;
}
private function buildPropertyView(
PhabricatorUser $user) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($user);
$field_list = PhabricatorCustomField::getObjectFields(
$user,
PhabricatorCustomField::ROLE_VIEW);
$field_list->appendFieldsToPropertyList($user, $viewer, $view);
if (!$view->hasAnyProperties()) {
return null;
}
$header = id(new PHUIHeaderView())
->setHeader(pht('User Details'));
$view = id(new PHUIObjectBoxView())
->appendChild($view)
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addClass('project-view-properties');
return $view;
}
private function buildProjectsView(
PhabricatorUser $user) {
$viewer = $this->getViewer();
$projects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withMemberPHIDs(array($user->getPHID()))
->needImages(true)
->setOrder('newest') //c4science custo
->withStatuses(
array(
PhabricatorProjectStatus::STATUS_ACTIVE,
))
->execute();
$header = id(new PHUIHeaderView())
->setHeader(pht('Projects'));
if (!empty($projects)) {
$limit = 5;
$render_phids = array_slice($projects, 0, $limit);
$list = id(new PhabricatorProjectListView())
->setUser($viewer)
->setProjects($render_phids);
if (count($projects) > $limit) {
$header_text = pht(
'Projects (%s)',
phutil_count($projects));
$header = id(new PHUIHeaderView())
->setHeader($header_text)
->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-list-ul')
->setText(pht('View All'))
// c4science custo, anchor to results and order
->setHref('/project/?order=newest&member='.$user->getPHID().'#R'));
}
} else {
$list = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NODATA)
->appendChild(pht('User does not belong to any projects.'));
}
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($list)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
return $box;
}
private function buildCalendarDayView(PhabricatorUser $user) {
$viewer = $this->getViewer();
$class = 'PhabricatorCalendarApplication';
if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) {
return null;
}
+ // Don't show calendar information for disabled users, since it's probably
+ // not useful or accurate and may be misleading.
+ if ($user->getIsDisabled()) {
+ return null;
+ }
+
$midnight = PhabricatorTime::getTodayMidnightDateTime($viewer);
$week_end = clone $midnight;
$week_end = $week_end->modify('+3 days');
$range_start = $midnight->format('U');
$range_end = $week_end->format('U');
$events = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withDateRange($range_start, $range_end)
->withInvitedPHIDs(array($user->getPHID()))
->withIsCancelled(false)
->needRSVPs(array($viewer->getPHID()))
->execute();
$event_views = array();
foreach ($events as $event) {
$viewer_is_invited = $event->isRSVPInvited($viewer->getPHID());
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$event,
PhabricatorPolicyCapability::CAN_EDIT);
$epoch_min = $event->getStartDateTimeEpoch();
$epoch_max = $event->getEndDateTimeEpoch();
$event_view = id(new AphrontCalendarEventView())
->setCanEdit($can_edit)
->setEventID($event->getID())
->setEpochRange($epoch_min, $epoch_max)
->setIsAllDay($event->getIsAllDay())
->setIcon($event->getIcon())
->setViewerIsInvited($viewer_is_invited)
->setName($event->getName())
->setDatetimeSummary($event->renderEventDate($viewer, true))
->setURI($event->getURI());
$event_views[] = $event_view;
}
$event_views = msort($event_views, 'getEpochStart');
$day_view = id(new PHUICalendarWeekView())
->setViewer($viewer)
->setView('week')
->setEvents($event_views)
->setWeekLength(3)
->render();
$header = id(new PHUIHeaderView())
->setHeader(pht('Calendar'))
->setHref(
urisprintf(
'/calendar/?invited=%s#R',
$user->getUsername()));
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($day_view)
->addClass('calendar-profile-box')
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
return $box;
}
private function buildPeopleFeed(
PhabricatorUser $user,
$viewer) {
$query = new PhabricatorFeedQuery();
$query->withFilterPHIDs(
array(
$user->getPHID(),
));
$query->setLimit(100);
$query->setViewer($viewer);
$stories = $query->execute();
$builder = new PhabricatorFeedBuilder($stories);
$builder->setUser($viewer);
$builder->setShowHovercards(true);
$builder->setNoDataString(pht('To begin on such a grand journey, '.
'requires but just a single step.'));
$view = $builder->buildView();
return $view->render();
}
}
diff --git a/src/applications/people/customfield/PhabricatorUserStatusField.php b/src/applications/people/customfield/PhabricatorUserStatusField.php
index 2ae915856..1716e8e19 100644
--- a/src/applications/people/customfield/PhabricatorUserStatusField.php
+++ b/src/applications/people/customfield/PhabricatorUserStatusField.php
@@ -1,38 +1,44 @@
<?php
final class PhabricatorUserStatusField
extends PhabricatorUserCustomField {
private $value;
public function getFieldKey() {
return 'user:status';
}
public function getFieldName() {
return pht('Availability');
}
public function getFieldDescription() {
return pht('Shows when a user is away or busy.');
}
public function shouldAppearInPropertyView() {
return true;
}
public function isFieldEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorCalendarApplication');
}
public function renderPropertyViewValue(array $handles) {
$user = $this->getObject();
$viewer = $this->requireViewer();
+ // Don't show availability for disabled users, since this is vaguely
+ // misleading to say "Availability: Available" and probably not useful.
+ if ($user->getIsDisabled()) {
+ return null;
+ }
+
return id(new PHUIUserAvailabilityView())
->setViewer($viewer)
->setAvailableUser($user);
}
}
diff --git a/src/applications/people/mail/PhabricatorPeopleUsernameMailEngine.php b/src/applications/people/mail/PhabricatorPeopleUsernameMailEngine.php
new file mode 100644
index 000000000..c954b7c38
--- /dev/null
+++ b/src/applications/people/mail/PhabricatorPeopleUsernameMailEngine.php
@@ -0,0 +1,60 @@
+<?php
+
+final class PhabricatorPeopleUsernameMailEngine
+ extends PhabricatorPeopleMailEngine {
+
+ private $oldUsername;
+ private $newUsername;
+
+ public function setNewUsername($new_username) {
+ $this->newUsername = $new_username;
+ return $this;
+ }
+
+ public function getNewUsername() {
+ return $this->newUsername;
+ }
+
+ public function setOldUsername($old_username) {
+ $this->oldUsername = $old_username;
+ return $this;
+ }
+
+ public function getOldUsername() {
+ return $this->oldUsername;
+ }
+
+ public function validateMail() {
+ return;
+ }
+
+ protected function newMail() {
+ $sender = $this->getSender();
+ $recipient = $this->getRecipient();
+
+ $sender_username = $sender->getUsername();
+ $sender_realname = $sender->getRealName();
+
+ $old_username = $this->getOldUsername();
+ $new_username = $this->getNewUsername();
+
+ $body = sprintf(
+ "%s\n\n %s\n %s\n",
+ pht(
+ '%s (%s) has changed your Phabricator username.',
+ $sender_username,
+ $sender_realname),
+ pht(
+ 'Old Username: %s',
+ $old_username),
+ pht(
+ 'New Username: %s',
+ $new_username));
+
+ return id(new PhabricatorMetaMTAMail())
+ ->addTos(array($recipient->getPHID()))
+ ->setSubject(pht('[Phabricator] Username Changed'))
+ ->setBody($body);
+ }
+
+}
diff --git a/src/applications/people/query/PhabricatorPeopleQuery.php b/src/applications/people/query/PhabricatorPeopleQuery.php
index c7d0b4b1a..6a50c0274 100644
--- a/src/applications/people/query/PhabricatorPeopleQuery.php
+++ b/src/applications/people/query/PhabricatorPeopleQuery.php
@@ -1,660 +1,659 @@
<?php
final class PhabricatorPeopleQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $usernames;
private $realnames;
private $emails;
private $phids;
private $ids;
private $dateCreatedAfter;
private $dateCreatedBefore;
private $isAdmin;
private $isSystemAgent;
private $isMailingList;
private $isDisabled;
private $isApproved;
private $nameLike;
private $nameTokens;
private $namePrefixes;
private $isEnrolledInMultiFactor;
private $memberOf; // c4science custo
private $needPrimaryEmail;
private $needProfile;
private $needProfileImage;
private $needAvailability;
private $needBadgeAwards;
private $cacheKeys = array();
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withEmails(array $emails) {
$this->emails = $emails;
return $this;
}
public function withRealnames(array $realnames) {
$this->realnames = $realnames;
return $this;
}
public function withUsernames(array $usernames) {
$this->usernames = $usernames;
return $this;
}
public function withDateCreatedBefore($date_created_before) {
$this->dateCreatedBefore = $date_created_before;
return $this;
}
public function withDateCreatedAfter($date_created_after) {
$this->dateCreatedAfter = $date_created_after;
return $this;
}
public function withIsAdmin($admin) {
$this->isAdmin = $admin;
return $this;
}
public function withIsSystemAgent($system_agent) {
$this->isSystemAgent = $system_agent;
return $this;
}
public function withIsMailingList($mailing_list) {
$this->isMailingList = $mailing_list;
return $this;
}
public function withIsDisabled($disabled) {
$this->isDisabled = $disabled;
return $this;
}
public function withIsApproved($approved) {
$this->isApproved = $approved;
return $this;
}
public function withNameLike($like) {
$this->nameLike = $like;
return $this;
}
public function withNameTokens(array $tokens) {
$this->nameTokens = array_values($tokens);
return $this;
}
public function withNamePrefixes(array $prefixes) {
$this->namePrefixes = $prefixes;
return $this;
}
public function withIsEnrolledInMultiFactor($enrolled) {
$this->isEnrolledInMultiFactor = $enrolled;
return $this;
}
// c4science customization
public function withMemberOf(array $projects) {
$this->memberOf = $projects;
return $this;
}
public function needPrimaryEmail($need) {
$this->needPrimaryEmail = $need;
return $this;
}
public function needProfile($need) {
$this->needProfile = $need;
return $this;
}
public function needProfileImage($need) {
$cache_key = PhabricatorUserProfileImageCacheType::KEY_URI;
if ($need) {
$this->cacheKeys[$cache_key] = true;
} else {
unset($this->cacheKeys[$cache_key]);
}
return $this;
}
public function needAvailability($need) {
$this->needAvailability = $need;
return $this;
}
public function needUserSettings($need) {
$cache_key = PhabricatorUserPreferencesCacheType::KEY_PREFERENCES;
if ($need) {
$this->cacheKeys[$cache_key] = true;
} else {
unset($this->cacheKeys[$cache_key]);
}
return $this;
}
public function needBadgeAwards($need) {
$cache_key = PhabricatorUserBadgesCacheType::KEY_BADGES;
if ($need) {
$this->cacheKeys[$cache_key] = true;
} else {
unset($this->cacheKeys[$cache_key]);
}
return $this;
}
public function newResultObject() {
return new PhabricatorUser();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function didFilterPage(array $users) {
if ($this->needProfile) {
$user_list = mpull($users, null, 'getPHID');
$profiles = new PhabricatorUserProfile();
$profiles = $profiles->loadAllWhere(
'userPHID IN (%Ls)',
array_keys($user_list));
$profiles = mpull($profiles, null, 'getUserPHID');
foreach ($user_list as $user_phid => $user) {
$profile = idx($profiles, $user_phid);
if (!$profile) {
$profile = PhabricatorUserProfile::initializeNewProfile($user);
}
$user->attachUserProfile($profile);
}
}
if ($this->needAvailability) {
$rebuild = array();
foreach ($users as $user) {
$cache = $user->getAvailabilityCache();
if ($cache !== null) {
$user->attachAvailability($cache);
} else {
$rebuild[] = $user;
}
}
if ($rebuild) {
$this->rebuildAvailabilityCache($rebuild);
}
}
$this->fillUserCaches($users);
return $users;
}
protected function shouldGroupQueryResultRows() {
if ($this->nameTokens) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->emails) {
$email_table = new PhabricatorUserEmail();
$joins[] = qsprintf(
$conn,
'JOIN %T email ON email.userPHID = user.PHID',
$email_table->getTableName());
}
if ($this->nameTokens) {
foreach ($this->nameTokens as $key => $token) {
$token_table = 'token_'.$key;
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.userID = user.id AND %T.token LIKE %>',
PhabricatorUser::NAMETOKEN_TABLE,
$token_table,
$token_table,
$token_table,
$token);
}
}
// c4science customization
if($this->memberOf) {
$joins[] = qsprintf(
$conn,
'JOIN %T.%T e ON e.dst = user.PHID AND e.type = %d',
'phabricator_project',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorProjectMaterializedMemberEdgeType::EDGECONST);
}
return $joins;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->usernames !== null) {
$where[] = qsprintf(
$conn,
'user.userName IN (%Ls)',
$this->usernames);
}
if ($this->namePrefixes) {
$parts = array();
foreach ($this->namePrefixes as $name_prefix) {
$parts[] = qsprintf(
$conn,
'user.username LIKE %>',
$name_prefix);
}
$where[] = qsprintf($conn, '%LO', $parts);
}
if ($this->emails !== null) {
$where[] = qsprintf(
$conn,
'email.address IN (%Ls)',
$this->emails);
}
if ($this->realnames !== null) {
$where[] = qsprintf(
$conn,
'user.realName IN (%Ls)',
$this->realnames);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'user.phid IN (%Ls)',
$this->phids);
}
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'user.id IN (%Ld)',
$this->ids);
}
if ($this->dateCreatedAfter) {
$where[] = qsprintf(
$conn,
'user.dateCreated >= %d',
$this->dateCreatedAfter);
}
if ($this->dateCreatedBefore) {
$where[] = qsprintf(
$conn,
'user.dateCreated <= %d',
$this->dateCreatedBefore);
}
if ($this->isAdmin !== null) {
$where[] = qsprintf(
$conn,
'user.isAdmin = %d',
(int)$this->isAdmin);
}
if ($this->isDisabled !== null) {
$where[] = qsprintf(
$conn,
'user.isDisabled = %d',
(int)$this->isDisabled);
}
if ($this->isApproved !== null) {
$where[] = qsprintf(
$conn,
'user.isApproved = %d',
(int)$this->isApproved);
}
if ($this->isSystemAgent !== null) {
$where[] = qsprintf(
$conn,
'user.isSystemAgent = %d',
(int)$this->isSystemAgent);
}
if ($this->isMailingList !== null) {
$where[] = qsprintf(
$conn,
'user.isMailingList = %d',
(int)$this->isMailingList);
}
if (strlen($this->nameLike)) {
$where[] = qsprintf(
$conn,
'user.username LIKE %~ OR user.realname LIKE %~',
$this->nameLike,
$this->nameLike);
}
if ($this->isEnrolledInMultiFactor !== null) {
$where[] = qsprintf(
$conn,
'user.isEnrolledInMultiFactor = %d',
(int)$this->isEnrolledInMultiFactor);
}
// c4science customization
if ($this->memberOf) {
$where[] = qsprintf(
$conn,
'e.src IN (%Ls)',
$this->memberOf);
}
return $where;
}
protected function getPrimaryTableAlias() {
return 'user';
}
public function getQueryApplicationClass() {
return 'PhabricatorPeopleApplication';
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'username' => array(
'table' => 'user',
'column' => 'username',
'type' => 'string',
'reverse' => true,
'unique' => true,
),
);
}
- protected function getPagingValueMap($cursor, array $keys) {
- $user = $this->loadCursorObject($cursor);
+ protected function newPagingMapFromPartialObject($object) {
return array(
- 'id' => $user->getID(),
- 'username' => $user->getUsername(),
+ 'id' => (int)$object->getID(),
+ 'username' => $object->getUsername(),
);
}
private function rebuildAvailabilityCache(array $rebuild) {
$rebuild = mpull($rebuild, null, 'getPHID');
// Limit the window we look at because far-future events are largely
// irrelevant and this makes the cache cheaper to build and allows it to
// self-heal over time.
$min_range = PhabricatorTime::getNow();
$max_range = $min_range + phutil_units('72 hours in seconds');
// NOTE: We don't need to generate ghosts here, because we only care if
// the user is attending, and you can't attend a ghost event: RSVP'ing
// to it creates a real event.
$events = id(new PhabricatorCalendarEventQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withInvitedPHIDs(array_keys($rebuild))
->withIsCancelled(false)
->withDateRange($min_range, $max_range)
->execute();
// Group all the events by invited user. Only examine events that users
// are actually attending.
$map = array();
$invitee_map = array();
foreach ($events as $event) {
foreach ($event->getInvitees() as $invitee) {
if (!$invitee->isAttending()) {
continue;
}
// If the user is set to "Available" for this event, don't consider it
// when computing their away status.
if (!$invitee->getDisplayAvailability($event)) {
continue;
}
$invitee_phid = $invitee->getInviteePHID();
if (!isset($rebuild[$invitee_phid])) {
continue;
}
$map[$invitee_phid][] = $event;
$event_phid = $event->getPHID();
$invitee_map[$invitee_phid][$event_phid] = $invitee;
}
}
// We need to load these users' timezone settings to figure out their
// availability if they're attending all-day events.
$this->needUserSettings(true);
$this->fillUserCaches($rebuild);
foreach ($rebuild as $phid => $user) {
$events = idx($map, $phid, array());
// We loaded events with the omnipotent user, but want to shift them
// into the user's timezone before building the cache because they will
// be unavailable during their own local day.
foreach ($events as $event) {
$event->applyViewerTimezone($user);
}
$cursor = $min_range;
$next_event = null;
if ($events) {
// Find the next time when the user has no meetings. If we move forward
// because of an event, we check again for events after that one ends.
while (true) {
foreach ($events as $event) {
$from = $event->getStartDateTimeEpochForCache();
$to = $event->getEndDateTimeEpochForCache();
if (($from <= $cursor) && ($to > $cursor)) {
$cursor = $to;
if (!$next_event) {
$next_event = $event;
}
continue 2;
}
}
break;
}
}
if ($cursor > $min_range) {
$invitee = $invitee_map[$phid][$next_event->getPHID()];
$availability_type = $invitee->getDisplayAvailability($next_event);
$availability = array(
'until' => $cursor,
'eventPHID' => $next_event->getPHID(),
'availability' => $availability_type,
);
// We only cache this availability until the end of the current event,
// since the event PHID (and possibly the availability type) are only
// valid for that long.
// NOTE: This doesn't handle overlapping events with the greatest
// possible care. In theory, if you're attending multiple events
// simultaneously we should accommodate that. However, it's complex
// to compute, rare, and probably not confusing most of the time.
$availability_ttl = $next_event->getEndDateTimeEpochForCache();
} else {
$availability = array(
'until' => null,
'eventPHID' => null,
'availability' => null,
);
// Cache that the user is available until the next event they are
// invited to starts.
$availability_ttl = $max_range;
foreach ($events as $event) {
$from = $event->getStartDateTimeEpochForCache();
if ($from > $cursor) {
$availability_ttl = min($from, $availability_ttl);
}
}
}
// Never TTL the cache to longer than the maximum range we examined.
$availability_ttl = min($availability_ttl, $max_range);
$user->writeAvailabilityCache($availability, $availability_ttl);
$user->attachAvailability($availability);
}
}
private function fillUserCaches(array $users) {
if (!$this->cacheKeys) {
return;
}
$user_map = mpull($users, null, 'getPHID');
$keys = array_keys($this->cacheKeys);
$hashes = array();
foreach ($keys as $key) {
$hashes[] = PhabricatorHash::digestForIndex($key);
}
$types = PhabricatorUserCacheType::getAllCacheTypes();
// First, pull any available caches. If we wanted to be particularly clever
// we could do this with JOINs in the main query.
$cache_table = new PhabricatorUserCache();
$cache_conn = $cache_table->establishConnection('r');
$cache_data = queryfx_all(
$cache_conn,
'SELECT cacheKey, userPHID, cacheData, cacheType FROM %T
WHERE cacheIndex IN (%Ls) AND userPHID IN (%Ls)',
$cache_table->getTableName(),
$hashes,
array_keys($user_map));
$skip_validation = array();
// After we read caches from the database, discard any which have data that
// invalid or out of date. This allows cache types to implement TTLs or
// versions instead of or in addition to explicit cache clears.
foreach ($cache_data as $row_key => $row) {
$cache_type = $row['cacheType'];
if (isset($skip_validation[$cache_type])) {
continue;
}
if (empty($types[$cache_type])) {
unset($cache_data[$row_key]);
continue;
}
$type = $types[$cache_type];
if (!$type->shouldValidateRawCacheData()) {
$skip_validation[$cache_type] = true;
continue;
}
$user = $user_map[$row['userPHID']];
$raw_data = $row['cacheData'];
if (!$type->isRawCacheDataValid($user, $row['cacheKey'], $raw_data)) {
unset($cache_data[$row_key]);
continue;
}
}
$need = array();
$cache_data = igroup($cache_data, 'userPHID');
foreach ($user_map as $user_phid => $user) {
$raw_rows = idx($cache_data, $user_phid, array());
$raw_data = ipull($raw_rows, 'cacheData', 'cacheKey');
foreach ($keys as $key) {
if (isset($raw_data[$key]) || array_key_exists($key, $raw_data)) {
continue;
}
$need[$key][$user_phid] = $user;
}
$user->attachRawCacheData($raw_data);
}
// If we missed any cache values, bulk-construct them now. This is
// usually much cheaper than generating them on-demand for each user
// record.
if (!$need) {
return;
}
$writes = array();
foreach ($need as $cache_key => $need_users) {
$type = PhabricatorUserCacheType::getCacheTypeForKey($cache_key);
if (!$type) {
continue;
}
$data = $type->newValueForUsers($cache_key, $need_users);
foreach ($data as $user_phid => $raw_value) {
$data[$user_phid] = $raw_value;
$writes[] = array(
'userPHID' => $user_phid,
'key' => $cache_key,
'type' => $type,
'value' => $raw_value,
);
}
foreach ($need_users as $user_phid => $user) {
if (isset($data[$user_phid]) || array_key_exists($user_phid, $data)) {
$user->attachRawCacheData(
array(
$cache_key => $data[$user_phid],
));
}
}
}
PhabricatorUserCache::writeCaches($writes);
}
}
diff --git a/src/applications/people/storage/PhabricatorExternalAccount.php b/src/applications/people/storage/PhabricatorExternalAccount.php
index 4bc0fcae9..bde958833 100644
--- a/src/applications/people/storage/PhabricatorExternalAccount.php
+++ b/src/applications/people/storage/PhabricatorExternalAccount.php
@@ -1,163 +1,165 @@
<?php
final class PhabricatorExternalAccount extends PhabricatorUserDAO
implements PhabricatorPolicyInterface {
protected $userPHID;
protected $accountType;
protected $accountDomain;
protected $accountSecret;
protected $accountID;
protected $displayName;
protected $username;
protected $realName;
protected $email;
protected $emailVerified = 0;
protected $accountURI;
protected $profileImagePHID;
protected $properties = array();
+ protected $providerConfigPHID;
private $profileImageFile = self::ATTACHABLE;
+ private $providerConfig = self::ATTACHABLE;
public function getProfileImageFile() {
return $this->assertAttached($this->profileImageFile);
}
public function attachProfileImageFile(PhabricatorFile $file) {
$this->profileImageFile = $file;
return $this;
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPeopleExternalPHIDType::TYPECONST);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'userPHID' => 'phid?',
'accountType' => 'text16',
'accountDomain' => 'text64',
'accountSecret' => 'text?',
'accountID' => 'text64',
'displayName' => 'text255?',
'username' => 'text255?',
'realName' => 'text255?',
'email' => 'text255?',
'emailVerified' => 'bool',
'profileImagePHID' => 'phid?',
'accountURI' => 'text255?',
),
self::CONFIG_KEY_SCHEMA => array(
'account_details' => array(
'columns' => array('accountType', 'accountDomain', 'accountID'),
'unique' => true,
),
'key_user' => array(
'columns' => array('userPHID'),
),
),
) + parent::getConfiguration();
}
- public function getPhabricatorUser() {
- $tmp_usr = id(new PhabricatorUser())
- ->makeEphemeral()
- ->setPHID($this->getPHID());
- return $tmp_usr;
- }
-
public function getProviderKey() {
return $this->getAccountType().':'.$this->getAccountDomain();
}
public function save() {
if (!$this->getAccountSecret()) {
$this->setAccountSecret(Filesystem::readRandomCharacters(32));
}
return parent::save();
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function isUsableForLogin() {
- $key = $this->getProviderKey();
- $provider = PhabricatorAuthProvider::getEnabledProviderByKey($key);
-
- if (!$provider) {
+ $config = $this->getProviderConfig();
+ if (!$config->getIsEnabled()) {
return false;
}
+ $provider = $config->getProvider();
if (!$provider->shouldAllowLogin()) {
return false;
}
return true;
}
public function getDisplayName() {
if (strlen($this->displayName)) {
return $this->displayName;
}
// TODO: Figure out how much identifying information we're going to show
// to users about external accounts. For now, just show a string which is
// clearly not an error, but don't disclose any identifying information.
$map = array(
'email' => pht('Email User'),
);
$type = $this->getAccountType();
return idx($map, $type, pht('"%s" User', $type));
}
+ public function attachProviderConfig(PhabricatorAuthProviderConfig $config) {
+ $this->providerConfig = $config;
+ return $this;
+ }
+
+ public function getProviderConfig() {
+ return $this->assertAttached($this->providerConfig);
+ }
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::POLICY_NOONE;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return ($viewer->getPHID() == $this->getUserPHID());
}
public function describeAutomaticCapability($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return null;
case PhabricatorPolicyCapability::CAN_EDIT:
return pht(
'External accounts can only be edited by the account owner.');
}
}
}
diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php
index 42228f6bd..aa5f04f9a 100644
--- a/src/applications/people/storage/PhabricatorUser.php
+++ b/src/applications/people/storage/PhabricatorUser.php
@@ -1,1582 +1,1534 @@
<?php
/**
* @task availability Availability
* @task image-cache Profile Image Cache
* @task factors Multi-Factor Authentication
* @task handles Managing Handles
* @task settings Settings
* @task cache User Cache
*/
final class PhabricatorUser
extends PhabricatorUserDAO
implements
PhutilPerson,
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface,
PhabricatorSSHPublicKeyInterface,
PhabricatorFlaggableInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorConduitResultInterface,
PhabricatorAuthPasswordHashInterface {
const SESSION_TABLE = 'phabricator_session';
const NAMETOKEN_TABLE = 'user_nametoken';
const MAXIMUM_USERNAME_LENGTH = 64;
protected $userName;
protected $realName;
protected $profileImagePHID;
protected $defaultProfileImagePHID;
protected $defaultProfileImageVersion;
protected $availabilityCache;
protected $availabilityCacheTTL;
protected $conduitCertificate;
protected $isSystemAgent = 0;
protected $isMailingList = 0;
protected $isAdmin = 0;
protected $isDisabled = 0;
protected $isEmailVerified = 0;
protected $isApproved = 0;
protected $isEnrolledInMultiFactor = 0;
protected $accountSecret;
private $profile = null;
private $availability = self::ATTACHABLE;
private $preferences = null;
private $omnipotent = false;
private $customFields = self::ATTACHABLE;
private $badgePHIDs = self::ATTACHABLE;
private $alternateCSRFString = self::ATTACHABLE;
private $session = self::ATTACHABLE;
private $rawCacheData = array();
private $usableCacheData = array();
private $authorities = array();
private $handlePool;
private $csrfSalt;
private $settingCacheKeys = array();
private $settingCache = array();
private $allowInlineCacheGeneration;
private $conduitClusterToken = self::ATTACHABLE;
protected function readField($field) {
switch ($field) {
// Make sure these return booleans.
case 'isAdmin':
return (bool)$this->isAdmin;
case 'isDisabled':
return (bool)$this->isDisabled;
case 'isSystemAgent':
return (bool)$this->isSystemAgent;
case 'isMailingList':
return (bool)$this->isMailingList;
case 'isEmailVerified':
return (bool)$this->isEmailVerified;
case 'isApproved':
return (bool)$this->isApproved;
default:
return parent::readField($field);
}
}
/**
* Is this a live account which has passed required approvals? Returns true
* if this is an enabled, verified (if required), approved (if required)
* account, and false otherwise.
*
* @return bool True if this is a standard, usable account.
*/
public function isUserActivated() {
if (!$this->isLoggedIn()) {
return false;
}
if ($this->isOmnipotent()) {
return true;
}
if ($this->getIsDisabled()) {
return false;
}
if (!$this->getIsApproved()) {
return false;
}
if (PhabricatorUserEmail::isEmailVerificationRequired()) {
if (!$this->getIsEmailVerified()) {
return false;
}
}
return true;
}
/**
* Is this a user who we can reasonably expect to respond to requests?
*
* This is used to provide a grey "disabled/unresponsive" dot cue when
* rendering handles and tags, so it isn't a surprise if you get ignored
* when you ask things of users who will not receive notifications or could
* not respond to them (because they are disabled, unapproved, do not have
* verified email addresses, etc).
*
* @return bool True if this user can receive and respond to requests from
* other humans.
*/
public function isResponsive() {
if (!$this->isUserActivated()) {
return false;
}
if (!$this->getIsEmailVerified()) {
return false;
}
return true;
}
public function canEstablishWebSessions() {
if ($this->getIsMailingList()) {
return false;
}
if ($this->getIsSystemAgent()) {
return false;
}
return true;
}
public function canEstablishAPISessions() {
if ($this->getIsDisabled()) {
return false;
}
// Intracluster requests are permitted even if the user is logged out:
// in particular, public users are allowed to issue intracluster requests
// when browsing Diffusion.
if (PhabricatorEnv::isClusterRemoteAddress()) {
if (!$this->isLoggedIn()) {
return true;
}
}
if (!$this->isUserActivated()) {
return false;
}
if ($this->getIsMailingList()) {
return false;
}
return true;
}
public function canEstablishSSHSessions() {
if (!$this->isUserActivated()) {
return false;
}
if ($this->getIsMailingList()) {
return false;
}
return true;
}
/**
* Returns `true` if this is a standard user who is logged in. Returns `false`
* for logged out, anonymous, or external users.
*
* @return bool `true` if the user is a standard user who is logged in with
* a normal session.
*/
public function getIsStandardUser() {
$type_user = PhabricatorPeopleUserPHIDType::TYPECONST;
return $this->getPHID() && (phid_get_type($this->getPHID()) == $type_user);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'userName' => 'sort64',
'realName' => 'text128',
'profileImagePHID' => 'phid?',
'conduitCertificate' => 'text255',
'isSystemAgent' => 'bool',
'isMailingList' => 'bool',
'isDisabled' => 'bool',
'isAdmin' => 'bool',
'isEmailVerified' => 'uint32',
'isApproved' => 'uint32',
'accountSecret' => 'bytes64',
'isEnrolledInMultiFactor' => 'bool',
'availabilityCache' => 'text255?',
'availabilityCacheTTL' => 'uint32?',
'defaultProfileImagePHID' => 'phid?',
'defaultProfileImageVersion' => 'text64?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'userName' => array(
'columns' => array('userName'),
'unique' => true,
),
'realName' => array(
'columns' => array('realName'),
),
'key_approved' => array(
'columns' => array('isApproved'),
),
),
self::CONFIG_NO_MUTATE => array(
'availabilityCache' => true,
'availabilityCacheTTL' => true,
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPeopleUserPHIDType::TYPECONST);
}
public function getMonogram() {
return '@'.$this->getUsername();
}
public function isLoggedIn() {
return !($this->getPHID() === null);
}
public function saveWithoutIndex() {
return parent::save();
}
public function save() {
if (!$this->getConduitCertificate()) {
$this->setConduitCertificate($this->generateConduitCertificate());
}
if (!strlen($this->getAccountSecret())) {
$this->setAccountSecret(Filesystem::readRandomCharacters(64));
}
$result = $this->saveWithoutIndex();
if ($this->profile) {
$this->profile->save();
}
$this->updateNameTokens();
PhabricatorSearchWorker::queueDocumentForIndexing($this->getPHID());
return $result;
}
public function attachSession(PhabricatorAuthSession $session) {
$this->session = $session;
return $this;
}
public function getSession() {
return $this->assertAttached($this->session);
}
public function hasSession() {
return ($this->session !== self::ATTACHABLE);
}
public function hasHighSecuritySession() {
if (!$this->hasSession()) {
return false;
}
return $this->getSession()->isHighSecuritySession();
}
private function generateConduitCertificate() {
return Filesystem::readRandomCharacters(255);
}
const EMAIL_CYCLE_FREQUENCY = 86400;
const EMAIL_TOKEN_LENGTH = 24;
public function getUserProfile() {
return $this->assertAttached($this->profile);
}
public function attachUserProfile(PhabricatorUserProfile $profile) {
$this->profile = $profile;
return $this;
}
public function loadUserProfile() {
if ($this->profile) {
return $this->profile;
}
$profile_dao = new PhabricatorUserProfile();
$this->profile = $profile_dao->loadOneWhere('userPHID = %s',
$this->getPHID());
if (!$this->profile) {
$this->profile = PhabricatorUserProfile::initializeNewProfile($this);
}
return $this->profile;
}
public function loadPrimaryEmailAddress() {
$email = $this->loadPrimaryEmail();
if (!$email) {
throw new Exception(pht('User has no primary email address!'));
}
return $email->getAddress();
}
public function loadPrimaryEmail() {
return id(new PhabricatorUserEmail())->loadOneWhere(
'userPHID = %s AND isPrimary = 1',
$this->getPHID());
}
/* -( Settings )----------------------------------------------------------- */
public function getUserSetting($key) {
// NOTE: We store available keys and cached values separately to make it
// faster to check for `null` in the cache, which is common.
if (isset($this->settingCacheKeys[$key])) {
return $this->settingCache[$key];
}
$settings_key = PhabricatorUserPreferencesCacheType::KEY_PREFERENCES;
if ($this->getPHID()) {
$settings = $this->requireCacheData($settings_key);
} else {
$settings = $this->loadGlobalSettings();
}
if (array_key_exists($key, $settings)) {
$value = $settings[$key];
return $this->writeUserSettingCache($key, $value);
}
$cache = PhabricatorCaches::getRuntimeCache();
$cache_key = "settings.defaults({$key})";
$cache_map = $cache->getKeys(array($cache_key));
if ($cache_map) {
$value = $cache_map[$cache_key];
} else {
$defaults = PhabricatorSetting::getAllSettings();
if (isset($defaults[$key])) {
$value = id(clone $defaults[$key])
->setViewer($this)
->getSettingDefaultValue();
} else {
$value = null;
}
$cache->setKey($cache_key, $value);
}
return $this->writeUserSettingCache($key, $value);
}
/**
* Test if a given setting is set to a particular value.
*
* @param const Setting key.
* @param wild Value to compare.
* @return bool True if the setting has the specified value.
* @task settings
*/
public function compareUserSetting($key, $value) {
$actual = $this->getUserSetting($key);
return ($actual == $value);
}
private function writeUserSettingCache($key, $value) {
$this->settingCacheKeys[$key] = true;
$this->settingCache[$key] = $value;
return $value;
}
public function getTranslation() {
return $this->getUserSetting(PhabricatorTranslationSetting::SETTINGKEY);
}
public function getTimezoneIdentifier() {
return $this->getUserSetting(PhabricatorTimezoneSetting::SETTINGKEY);
}
public static function getGlobalSettingsCacheKey() {
return 'user.settings.globals.v1';
}
private function loadGlobalSettings() {
$cache_key = self::getGlobalSettingsCacheKey();
$cache = PhabricatorCaches::getMutableStructureCache();
$settings = $cache->getKey($cache_key);
if (!$settings) {
$preferences = PhabricatorUserPreferences::loadGlobalPreferences($this);
$settings = $preferences->getPreferences();
$cache->setKey($cache_key, $settings);
}
return $settings;
}
/**
* Override the user's timezone identifier.
*
* This is primarily useful for unit tests.
*
* @param string New timezone identifier.
* @return this
* @task settings
*/
public function overrideTimezoneIdentifier($identifier) {
$timezone_key = PhabricatorTimezoneSetting::SETTINGKEY;
$this->settingCacheKeys[$timezone_key] = true;
$this->settingCache[$timezone_key] = $identifier;
return $this;
}
public function getGender() {
return $this->getUserSetting(PhabricatorPronounSetting::SETTINGKEY);
}
public function loadEditorLink(
$path,
$line,
PhabricatorRepository $repository = null) {
$editor = $this->getUserSetting(PhabricatorEditorSetting::SETTINGKEY);
if (is_array($path)) {
$multi_key = PhabricatorEditorMultipleSetting::SETTINGKEY;
$multiedit = $this->getUserSetting($multi_key);
switch ($multiedit) {
case PhabricatorEditorMultipleSetting::VALUE_SPACES:
$path = implode(' ', $path);
break;
case PhabricatorEditorMultipleSetting::VALUE_SINGLE:
default:
return null;
}
}
if (!strlen($editor)) {
return null;
}
if ($repository) {
$callsign = $repository->getCallsign();
} else {
$callsign = null;
}
$uri = strtr($editor, array(
'%%' => '%',
'%f' => phutil_escape_uri($path),
'%l' => phutil_escape_uri($line),
'%r' => phutil_escape_uri($callsign),
));
// The resulting URI must have an allowed protocol. Otherwise, we'll return
// a link to an error page explaining the misconfiguration.
$ok = PhabricatorHelpEditorProtocolController::hasAllowedProtocol($uri);
if (!$ok) {
return '/help/editorprotocol/';
}
return (string)$uri;
}
/**
* Populate the nametoken table, which used to fetch typeahead results. When
* a user types "linc", we want to match "Abraham Lincoln" from on-demand
* typeahead sources. To do this, we need a separate table of name fragments.
*/
public function updateNameTokens() {
$table = self::NAMETOKEN_TABLE;
$conn_w = $this->establishConnection('w');
$tokens = PhabricatorTypeaheadDatasource::tokenizeString(
$this->getUserName().' '.$this->getRealName());
$sql = array();
foreach ($tokens as $token) {
$sql[] = qsprintf(
$conn_w,
'(%d, %s)',
$this->getID(),
$token);
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE userID = %d',
$table,
$this->getID());
if ($sql) {
queryfx(
$conn_w,
'INSERT INTO %T (userID, token) VALUES %LQ',
$table,
$sql);
}
}
- public function sendUsernameChangeEmail(
- PhabricatorUser $admin,
- $old_username) {
-
- $admin_username = $admin->getUserName();
- $admin_realname = $admin->getRealName();
- $new_username = $this->getUserName();
-
- $password_instructions = null;
- if (PhabricatorPasswordAuthProvider::getPasswordProvider()) {
- $engine = new PhabricatorAuthSessionEngine();
- $uri = $engine->getOneTimeLoginURI(
- $this,
- null,
- PhabricatorAuthSessionEngine::ONETIME_USERNAME);
- $password_instructions = sprintf(
- "%s\n\n %s\n\n%s\n",
- pht(
- "If you use a password to login, you'll need to reset it ".
- "before you can login again. You can reset your password by ".
- "following this link:"),
- $uri,
- pht(
- "And, of course, you'll need to use your new username to login ".
- "from now on. If you use OAuth to login, nothing should change."));
- }
-
- $body = sprintf(
- "%s\n\n %s\n %s\n\n%s",
- pht(
- '%s (%s) has changed your Phabricator username.',
- $admin_username,
- $admin_realname),
- pht(
- 'Old Username: %s',
- $old_username),
- pht(
- 'New Username: %s',
- $new_username),
- $password_instructions);
-
- $mail = id(new PhabricatorMetaMTAMail())
- ->addTos(array($this->getPHID()))
- ->setForceDelivery(true)
- ->setSubject(pht('[Phabricator] Username Changed'))
- ->setBody($body)
- ->saveAndSend();
- }
-
public static function describeValidUsername() {
return pht(
- 'Usernames must contain only numbers, letters, period, underscore and '.
+ 'Usernames must contain only numbers, letters, period, underscore, and '.
'hyphen, and can not end with a period. They must have no more than %d '.
'characters.',
new PhutilNumber(self::MAXIMUM_USERNAME_LENGTH));
}
public static function validateUsername($username) {
// NOTE: If you update this, make sure to update:
//
// - Remarkup rule for @mentions.
// - Routing rule for "/p/username/".
// - Unit tests, obviously.
// - describeValidUsername() method, above.
if (strlen($username) > self::MAXIMUM_USERNAME_LENGTH) {
return false;
}
return (bool)preg_match('/^[a-zA-Z0-9._-]*[a-zA-Z0-9_-]\z/', $username);
}
public static function getDefaultProfileImageURI() {
return celerity_get_resource_uri('/rsrc/image/avatar.png');
}
public function getProfileImageURI() {
$uri_key = PhabricatorUserProfileImageCacheType::KEY_URI;
return $this->requireCacheData($uri_key);
}
public function getUnreadNotificationCount() {
$notification_key = PhabricatorUserNotificationCountCacheType::KEY_COUNT;
return $this->requireCacheData($notification_key);
}
public function getUnreadMessageCount() {
$message_key = PhabricatorUserMessageCountCacheType::KEY_COUNT;
return $this->requireCacheData($message_key);
}
public function getRecentBadgeAwards() {
$badges_key = PhabricatorUserBadgesCacheType::KEY_BADGES;
return $this->requireCacheData($badges_key);
}
public function getFullName() {
if (strlen($this->getRealName())) {
return $this->getUsername().' ('.$this->getRealName().')';
} else {
return $this->getUsername();
}
}
public function getTimeZone() {
return new DateTimeZone($this->getTimezoneIdentifier());
}
public function getTimeZoneOffset() {
$timezone = $this->getTimeZone();
$now = new DateTime('@'.PhabricatorTime::getNow());
$offset = $timezone->getOffset($now);
// Javascript offsets are in minutes and have the opposite sign.
$offset = -(int)($offset / 60);
return $offset;
}
public function getTimeZoneOffsetInHours() {
$offset = $this->getTimeZoneOffset();
$offset = (int)round($offset / 60);
$offset = -$offset;
return $offset;
}
public function formatShortDateTime($when, $now = null) {
if ($now === null) {
$now = PhabricatorTime::getNow();
}
try {
$when = new DateTime('@'.$when);
$now = new DateTime('@'.$now);
} catch (Exception $ex) {
return null;
}
$zone = $this->getTimeZone();
$when->setTimeZone($zone);
$now->setTimeZone($zone);
if ($when->format('Y') !== $now->format('Y')) {
// Different year, so show "Feb 31 2075".
$format = 'M j Y';
} else if ($when->format('Ymd') !== $now->format('Ymd')) {
// Same year but different month and day, so show "Feb 31".
$format = 'M j';
} else {
// Same year, month and day so show a time of day.
$pref_time = PhabricatorTimeFormatSetting::SETTINGKEY;
$format = $this->getUserSetting($pref_time);
}
return $when->format($format);
}
public function __toString() {
return $this->getUsername();
}
public static function loadOneWithEmailAddress($address) {
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$address);
if (!$email) {
return null;
}
return id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$email->getUserPHID());
}
public function getDefaultSpacePHID() {
// TODO: We might let the user switch which space they're "in" later on;
// for now just use the global space if one exists.
// If the viewer has access to the default space, use that.
$spaces = PhabricatorSpacesNamespaceQuery::getViewerActiveSpaces($this);
foreach ($spaces as $space) {
if ($space->getIsDefaultNamespace()) {
return $space->getPHID();
}
}
// Otherwise, use the space with the lowest ID that they have access to.
// This just tends to keep the default stable and predictable over time,
// so adding a new space won't change behavior for users.
if ($spaces) {
$spaces = msort($spaces, 'getID');
return head($spaces)->getPHID();
}
return null;
}
/**
* Grant a user a source of authority, to let them bypass policy checks they
* could not otherwise.
*/
public function grantAuthority($authority) {
$this->authorities[] = $authority;
return $this;
}
/**
* Get authorities granted to the user.
*/
public function getAuthorities() {
return $this->authorities;
}
public function hasConduitClusterToken() {
return ($this->conduitClusterToken !== self::ATTACHABLE);
}
public function attachConduitClusterToken(PhabricatorConduitToken $token) {
$this->conduitClusterToken = $token;
return $this;
}
public function getConduitClusterToken() {
return $this->assertAttached($this->conduitClusterToken);
}
/* -( Availability )------------------------------------------------------- */
/**
* @task availability
*/
public function attachAvailability(array $availability) {
$this->availability = $availability;
return $this;
}
/**
* Get the timestamp the user is away until, if they are currently away.
*
* @return int|null Epoch timestamp, or `null` if the user is not away.
* @task availability
*/
public function getAwayUntil() {
$availability = $this->availability;
$this->assertAttached($availability);
if (!$availability) {
return null;
}
return idx($availability, 'until');
}
public function getDisplayAvailability() {
$availability = $this->availability;
$this->assertAttached($availability);
if (!$availability) {
return null;
}
$busy = PhabricatorCalendarEventInvitee::AVAILABILITY_BUSY;
return idx($availability, 'availability', $busy);
}
public function getAvailabilityEventPHID() {
$availability = $this->availability;
$this->assertAttached($availability);
if (!$availability) {
return null;
}
return idx($availability, 'eventPHID');
}
/**
* Get cached availability, if present.
*
* @return wild|null Cache data, or null if no cache is available.
* @task availability
*/
public function getAvailabilityCache() {
$now = PhabricatorTime::getNow();
if ($this->availabilityCacheTTL <= $now) {
return null;
}
try {
return phutil_json_decode($this->availabilityCache);
} catch (Exception $ex) {
return null;
}
}
/**
* Write to the availability cache.
*
* @param wild Availability cache data.
* @param int|null Cache TTL.
* @return this
* @task availability
*/
public function writeAvailabilityCache(array $availability, $ttl) {
if (PhabricatorEnv::isReadOnly()) {
return $this;
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$this->establishConnection('w'),
'UPDATE %T SET availabilityCache = %s, availabilityCacheTTL = %nd
WHERE id = %d',
$this->getTableName(),
phutil_json_encode($availability),
$ttl,
$this->getID());
unset($unguarded);
return $this;
}
/* -( Multi-Factor Authentication )---------------------------------------- */
/**
* Update the flag storing this user's enrollment in multi-factor auth.
*
* With certain settings, we need to check if a user has MFA on every page,
* so we cache MFA enrollment on the user object for performance. Calling this
* method synchronizes the cache by examining enrollment records. After
* updating the cache, use @{method:getIsEnrolledInMultiFactor} to check if
* the user is enrolled.
*
* This method should be called after any changes are made to a given user's
* multi-factor configuration.
*
* @return void
* @task factors
*/
public function updateMultiFactorEnrollment() {
$factors = id(new PhabricatorAuthFactorConfigQuery())
->setViewer($this)
->withUserPHIDs(array($this->getPHID()))
->withFactorProviderStatuses(
array(
PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
PhabricatorAuthFactorProviderStatus::STATUS_DEPRECATED,
))
->execute();
$enrolled = count($factors) ? 1 : 0;
if ($enrolled !== $this->isEnrolledInMultiFactor) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$this->establishConnection('w'),
'UPDATE %T SET isEnrolledInMultiFactor = %d WHERE id = %d',
$this->getTableName(),
$enrolled,
$this->getID());
unset($unguarded);
$this->isEnrolledInMultiFactor = $enrolled;
}
}
/**
* Check if the user is enrolled in multi-factor authentication.
*
* Enrolled users have one or more multi-factor authentication sources
* attached to their account. For performance, this value is cached. You
* can use @{method:updateMultiFactorEnrollment} to update the cache.
*
* @return bool True if the user is enrolled.
* @task factors
*/
public function getIsEnrolledInMultiFactor() {
return $this->isEnrolledInMultiFactor;
}
/* -( Omnipotence )-------------------------------------------------------- */
/**
* Returns true if this user is omnipotent. Omnipotent users bypass all policy
* checks.
*
* @return bool True if the user bypasses policy checks.
*/
public function isOmnipotent() {
// c4science custo
// Allow administrators to bypass all policies.
if ($this->getIsAdmin()) {
return true;
}
return $this->omnipotent;
}
/**
* Get an omnipotent user object for use in contexts where there is no acting
* user, notably daemons.
*
* @return PhabricatorUser An omnipotent user.
*/
public static function getOmnipotentUser() {
static $user = null;
if (!$user) {
$user = new PhabricatorUser();
$user->omnipotent = true;
$user->makeEphemeral();
}
return $user;
}
/**
* Get a scalar string identifying this user.
*
* This is similar to using the PHID, but distinguishes between omnipotent
* and public users explicitly. This allows safe construction of cache keys
* or cache buckets which do not conflate public and omnipotent users.
*
* @return string Scalar identifier.
*/
public function getCacheFragment() {
if ($this->isOmnipotent()) {
return 'u.omnipotent';
}
$phid = $this->getPHID();
if ($phid) {
return 'u.'.$phid;
}
return 'u.public';
}
/* -( Managing Handles )--------------------------------------------------- */
/**
* Get a @{class:PhabricatorHandleList} which benefits from this viewer's
* internal handle pool.
*
* @param list<phid> List of PHIDs to load.
* @return PhabricatorHandleList Handle list object.
* @task handle
*/
public function loadHandles(array $phids) {
if ($this->handlePool === null) {
$this->handlePool = id(new PhabricatorHandlePool())
->setViewer($this);
}
return $this->handlePool->newHandleList($phids);
}
/**
* Get a @{class:PHUIHandleView} for a single handle.
*
* This benefits from the viewer's internal handle pool.
*
* @param phid PHID to render a handle for.
* @return PHUIHandleView View of the handle.
* @task handle
*/
public function renderHandle($phid) {
return $this->loadHandles(array($phid))->renderHandle($phid);
}
/**
* Get a @{class:PHUIHandleListView} for a list of handles.
*
* This benefits from the viewer's internal handle pool.
*
* @param list<phid> List of PHIDs to render.
* @return PHUIHandleListView View of the handles.
* @task handle
*/
public function renderHandleList(array $phids) {
return $this->loadHandles($phids)->renderList();
}
public function attachBadgePHIDs(array $phids) {
$this->badgePHIDs = $phids;
return $this;
}
public function getBadgePHIDs() {
return $this->assertAttached($this->badgePHIDs);
}
/* -( CSRF )--------------------------------------------------------------- */
public function getCSRFToken() {
// c4science custo
// disable csrf for admin // FIXME: CHECK THIS
if ($this->isOmnipotent() && !$this->getIsAdmin()) {
// We may end up here when called from the daemons. The omnipotent user
// has no meaningful CSRF token, so just return `null`.
return null;
}
return $this->newCSRFEngine()
->newToken();
}
public function validateCSRFToken($token) {
return $this->newCSRFengine()
->isValidToken($token);
}
public function getAlternateCSRFString() {
return $this->assertAttached($this->alternateCSRFString);
}
public function attachAlternateCSRFString($string) {
$this->alternateCSRFString = $string;
return $this;
}
private function newCSRFEngine() {
if ($this->getPHID()) {
$vec = $this->getPHID().$this->getAccountSecret();
} else {
$vec = $this->getAlternateCSRFString();
}
if ($this->hasSession()) {
$vec = $vec.$this->getSession()->getSessionKey();
}
$engine = new PhabricatorAuthCSRFEngine();
if ($this->csrfSalt === null) {
$this->csrfSalt = $engine->newSalt();
}
$engine
->setSalt($this->csrfSalt)
->setSecret(new PhutilOpaqueEnvelope($vec));
return $engine;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::POLICY_PUBLIC;
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->getIsSystemAgent() || $this->getIsMailingList()) {
return PhabricatorPolicies::POLICY_ADMIN;
} else {
return PhabricatorPolicies::POLICY_NOONE;
}
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getPHID() && ($viewer->getPHID() === $this->getPHID());
}
public function describeAutomaticCapability($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_EDIT:
return pht('Only you can edit your information.');
default:
return null;
}
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('user.fields');
}
public function getCustomFieldBaseClass() {
return 'PhabricatorUserCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
- $externals = id(new PhabricatorExternalAccount())->loadAllWhere(
- 'userPHID = %s',
- $this->getPHID());
+ $externals = id(new PhabricatorExternalAccountQuery())
+ ->setViewer($engine->getViewer())
+ ->withUserPHIDs(array($this->getPHID()))
+ ->execute();
foreach ($externals as $external) {
$external->delete();
}
$prefs = id(new PhabricatorUserPreferencesQuery())
->setViewer($engine->getViewer())
->withUsers(array($this))
->execute();
foreach ($prefs as $pref) {
$engine->destroyObject($pref);
}
$profiles = id(new PhabricatorUserProfile())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($profiles as $profile) {
$profile->delete();
}
$keys = id(new PhabricatorAuthSSHKeyQuery())
->setViewer($engine->getViewer())
->withObjectPHIDs(array($this->getPHID()))
->execute();
foreach ($keys as $key) {
$engine->destroyObject($key);
}
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($emails as $email) {
$email->delete();
}
$sessions = id(new PhabricatorAuthSession())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($sessions as $session) {
$session->delete();
}
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($factors as $factor) {
$factor->delete();
}
$this->saveTransaction();
}
/* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */
public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) {
if ($viewer->getPHID() == $this->getPHID()) {
// If the viewer is managing their own keys, take them to the normal
// panel.
return '/settings/panel/ssh/';
} else {
// Otherwise, take them to the administrative panel for this user.
return '/settings/user/'.$this->getUsername().'/page/ssh/';
}
}
public function getSSHKeyDefaultName() {
return 'id_rsa_phabricator';
}
public function getSSHKeyNotifyPHIDs() {
return array(
$this->getPHID(),
);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorUserTransactionEditor();
}
public function getApplicationTransactionTemplate() {
return new PhabricatorUserTransaction();
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhabricatorUserFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new PhabricatorUserFerretEngine();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('username')
->setType('string')
->setDescription(pht("The user's username.")),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('realName')
->setType('string')
->setDescription(pht("The user's real name.")),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('roles')
->setType('list<string>')
->setDescription(pht('List of account roles.')),
);
}
public function getFieldValuesForConduit() {
$roles = array();
if ($this->getIsDisabled()) {
$roles[] = 'disabled';
}
if ($this->getIsSystemAgent()) {
$roles[] = 'bot';
}
if ($this->getIsMailingList()) {
$roles[] = 'list';
}
if ($this->getIsAdmin()) {
$roles[] = 'admin';
}
if ($this->getIsEmailVerified()) {
$roles[] = 'verified';
}
if ($this->getIsApproved()) {
$roles[] = 'approved';
}
if ($this->isUserActivated()) {
$roles[] = 'activated';
}
return array(
'username' => $this->getUsername(),
'realName' => $this->getRealName(),
'roles' => $roles,
);
}
public function getConduitSearchAttachments() {
return array(
id(new PhabricatorPeopleAvailabilitySearchEngineAttachment())
->setAttachmentKey('availability'),
);
}
/* -( User Cache )--------------------------------------------------------- */
/**
* @task cache
*/
public function attachRawCacheData(array $data) {
$this->rawCacheData = $data + $this->rawCacheData;
return $this;
}
public function setAllowInlineCacheGeneration($allow_cache_generation) {
$this->allowInlineCacheGeneration = $allow_cache_generation;
return $this;
}
/**
* @task cache
*/
protected function requireCacheData($key) {
if (isset($this->usableCacheData[$key])) {
return $this->usableCacheData[$key];
}
$type = PhabricatorUserCacheType::requireCacheTypeForKey($key);
if (isset($this->rawCacheData[$key])) {
$raw_value = $this->rawCacheData[$key];
$usable_value = $type->getValueFromStorage($raw_value);
$this->usableCacheData[$key] = $usable_value;
return $usable_value;
}
// By default, we throw if a cache isn't available. This is consistent
// with the standard `needX()` + `attachX()` + `getX()` interaction.
if (!$this->allowInlineCacheGeneration) {
throw new PhabricatorDataNotAttachedException($this);
}
$user_phid = $this->getPHID();
// Try to read the actual cache before we generate a new value. We can
// end up here via Conduit, which does not use normal sessions and can
// not pick up a free cache load during session identification.
if ($user_phid) {
$raw_data = PhabricatorUserCache::readCaches(
$type,
$key,
array($user_phid));
if (array_key_exists($user_phid, $raw_data)) {
$raw_value = $raw_data[$user_phid];
$usable_value = $type->getValueFromStorage($raw_value);
$this->rawCacheData[$key] = $raw_value;
$this->usableCacheData[$key] = $usable_value;
return $usable_value;
}
}
$usable_value = $type->getDefaultValue();
if ($user_phid) {
$map = $type->newValueForUsers($key, array($this));
if (array_key_exists($user_phid, $map)) {
$raw_value = $map[$user_phid];
$usable_value = $type->getValueFromStorage($raw_value);
$this->rawCacheData[$key] = $raw_value;
PhabricatorUserCache::writeCache(
$type,
$key,
$user_phid,
$raw_value);
}
}
$this->usableCacheData[$key] = $usable_value;
return $usable_value;
}
/**
* @task cache
*/
public function clearCacheData($key) {
unset($this->rawCacheData[$key]);
unset($this->usableCacheData[$key]);
return $this;
}
public function getCSSValue($variable_key) {
$preference = PhabricatorAccessibilitySetting::SETTINGKEY;
$key = $this->getUserSetting($preference);
$postprocessor = CelerityPostprocessor::getPostprocessor($key);
$variables = $postprocessor->getVariables();
if (!isset($variables[$variable_key])) {
throw new Exception(
pht(
'Unknown CSS variable "%s"!',
$variable_key));
}
return $variables[$variable_key];
}
/* -( PhabricatorAuthPasswordHashInterface )------------------------------- */
public function newPasswordDigest(
PhutilOpaqueEnvelope $envelope,
PhabricatorAuthPassword $password) {
// Before passwords are hashed, they are digested. The goal of digestion
// is twofold: to reduce the length of very long passwords to something
// reasonable; and to salt the password in case the best available hasher
// does not include salt automatically.
// Users may choose arbitrarily long passwords, and attackers may try to
// attack the system by probing it with very long passwords. When large
// inputs are passed to hashers -- which are intentionally slow -- it
// can result in unacceptably long runtimes. The classic attack here is
// to try to log in with a 64MB password and see if that locks up the
// machine for the next century. By digesting passwords to a standard
// length first, the length of the raw input does not impact the runtime
// of the hashing algorithm.
// Some hashers like bcrypt are self-salting, while other hashers are not.
// Applying salt while digesting passwords ensures that hashes are salted
// whether we ultimately select a self-salting hasher or not.
// For legacy compatibility reasons, old VCS and Account password digest
// algorithms are significantly more complicated than necessary to achieve
// these goals. This is because they once used a different hashing and
// salting process. When we upgraded to the modern modular hasher
// infrastructure, we just bolted it onto the end of the existing pipelines
// so that upgrading didn't break all users' credentials.
// New implementations can (and, generally, should) safely select the
// simple HMAC SHA256 digest at the bottom of the function, which does
// everything that a digest callback should without any needless legacy
// baggage on top.
if ($password->getLegacyDigestFormat() == 'v1') {
switch ($password->getPasswordType()) {
case PhabricatorAuthPassword::PASSWORD_TYPE_VCS:
// Old VCS passwords use an iterated HMAC SHA1 as a digest algorithm.
// They originally used this as a hasher, but it became a digest
// algorithm once hashing was upgraded to include bcrypt.
$digest = $envelope->openEnvelope();
$salt = $this->getPHID();
for ($ii = 0; $ii < 1000; $ii++) {
$digest = PhabricatorHash::weakDigest($digest, $salt);
}
return new PhutilOpaqueEnvelope($digest);
case PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT:
// Account passwords previously used this weird mess of salt and did
// not digest the input to a standard length.
// Beyond this being a weird special case, there are two actual
// problems with this, although neither are particularly severe:
// First, because we do not normalize the length of passwords, this
// algorithm may make us vulnerable to DOS attacks where an attacker
// attempts to use a very long input to slow down hashers.
// Second, because the username is part of the hash algorithm,
// renaming a user breaks their password. This isn't a huge deal but
// it's pretty silly. There's no security justification for this
// behavior, I just didn't think about the implication when I wrote
// it originally.
$parts = array(
$this->getUsername(),
$envelope->openEnvelope(),
$this->getPHID(),
$password->getPasswordSalt(),
);
return new PhutilOpaqueEnvelope(implode('', $parts));
}
}
// For passwords which do not have some crazy legacy reason to use some
// other digest algorithm, HMAC SHA256 is an excellent choice. It satisfies
// the digest requirements and is simple.
$digest = PhabricatorHash::digestHMACSHA256(
$envelope->openEnvelope(),
$password->getPasswordSalt());
return new PhutilOpaqueEnvelope($digest);
}
public function newPasswordBlocklist(
PhabricatorUser $viewer,
PhabricatorAuthPasswordEngine $engine) {
$list = array();
$list[] = $this->getUsername();
$list[] = $this->getRealName();
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($emails as $email) {
$list[] = $email->getAddress();
}
return $list;
}
}
diff --git a/src/applications/people/storage/PhabricatorUserEmail.php b/src/applications/people/storage/PhabricatorUserEmail.php
index 42946015d..572c7d6e8 100644
--- a/src/applications/people/storage/PhabricatorUserEmail.php
+++ b/src/applications/people/storage/PhabricatorUserEmail.php
@@ -1,275 +1,274 @@
<?php
/**
* @task restrictions Domain Restrictions
* @task email Email About Email
*/
final class PhabricatorUserEmail extends PhabricatorUserDAO {
protected $userPHID;
protected $address;
protected $isVerified;
protected $isPrimary;
protected $verificationCode;
const MAX_ADDRESS_LENGTH = 128;
protected function getConfiguration() {
return array(
self::CONFIG_COLUMN_SCHEMA => array(
'address' => 'sort128',
'isVerified' => 'bool',
'isPrimary' => 'bool',
'verificationCode' => 'text64?',
),
self::CONFIG_KEY_SCHEMA => array(
'address' => array(
'columns' => array('address'),
'unique' => true,
),
'userPHID' => array(
'columns' => array('userPHID', 'isPrimary'),
),
),
) + parent::getConfiguration();
}
public function getVerificationURI() {
return '/emailverify/'.$this->getVerificationCode().'/';
}
public function save() {
if (!$this->verificationCode) {
$this->setVerificationCode(Filesystem::readRandomCharacters(24));
}
return parent::save();
}
/* -( Domain Restrictions )------------------------------------------------ */
/**
* @task restrictions
*/
public static function isValidAddress($address) {
if (strlen($address) > self::MAX_ADDRESS_LENGTH) {
return false;
}
// Very roughly validate that this address isn't so mangled that a
// reasonable piece of code might completely misparse it. In particular,
// the major risks are:
//
// - `PhutilEmailAddress` needs to be able to extract the domain portion
// from it.
// - Reasonable mail adapters should be hard-pressed to interpret one
// address as several addresses.
//
// To this end, we're roughly verifying that there's some normal text, an
// "@" symbol, and then some more normal text.
$email_regex = '(^[a-z0-9_+.!-]+@[a-z0-9_+:.-]+\z)i';
if (!preg_match($email_regex, $address)) {
return false;
}
return true;
}
/**
* @task restrictions
*/
public static function describeValidAddresses() {
return pht(
- "Email addresses should be in the form '%s'. The maximum ".
- "length of an email address is %s character(s).",
- 'user@domain.com',
+ 'Email addresses should be in the form "user@domain.com". The maximum '.
+ 'length of an email address is %s characters.',
new PhutilNumber(self::MAX_ADDRESS_LENGTH));
}
/**
* @task restrictions
*/
public static function isAllowedAddress($address) {
if (!self::isValidAddress($address)) {
return false;
}
$allowed_domains = PhabricatorEnv::getEnvConfig('auth.email-domains');
if (!$allowed_domains) {
return true;
}
$addr_obj = new PhutilEmailAddress($address);
$domain = $addr_obj->getDomainName();
if (!$domain) {
return false;
}
$lower_domain = phutil_utf8_strtolower($domain);
foreach ($allowed_domains as $allowed_domain) {
$lower_allowed = phutil_utf8_strtolower($allowed_domain);
if ($lower_allowed === $lower_domain) {
return true;
}
}
return false;
}
/**
* @task restrictions
*/
public static function describeAllowedAddresses() {
$domains = PhabricatorEnv::getEnvConfig('auth.email-domains');
if (!$domains) {
return null;
}
if (count($domains) == 1) {
return pht('Email address must be @%s', head($domains));
} else {
return pht(
'Email address must be at one of: %s',
implode(', ', $domains));
}
}
/**
* Check if this install requires email verification.
*
* @return bool True if email addresses must be verified.
*
* @task restrictions
*/
public static function isEmailVerificationRequired() {
// NOTE: Configuring required email domains implies required verification.
return PhabricatorEnv::getEnvConfig('auth.require-email-verification') ||
PhabricatorEnv::getEnvConfig('auth.email-domains');
}
/* -( Email About Email )-------------------------------------------------- */
/**
* Send a verification email from $user to this address.
*
* @param PhabricatorUser The user sending the verification.
* @return this
* @task email
*/
public function sendVerificationEmail(PhabricatorUser $user) {
$username = $user->getUsername();
$address = $this->getAddress();
$link = PhabricatorEnv::getProductionURI($this->getVerificationURI());
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$signature = null;
if (!$is_serious) {
$signature = pht("Get Well Soon,\nPhabricator");
}
$body = sprintf(
"%s\n\n%s\n\n %s\n\n%s",
pht('Hi %s', $username),
pht(
'Please verify that you own this email address (%s) by '.
'clicking this link:',
$address),
$link,
$signature);
id(new PhabricatorMetaMTAMail())
->addRawTos(array($address))
->setForceDelivery(true)
->setSubject(pht('[Phabricator] Email Verification'))
->setBody($body)
->setRelatedPHID($user->getPHID())
->saveAndSend();
return $this;
}
/**
* Send a notification email from $user to this address, informing the
* recipient that this is no longer their account's primary address.
*
* @param PhabricatorUser The user sending the notification.
* @param PhabricatorUserEmail New primary email address.
* @return this
* @task email
*/
public function sendOldPrimaryEmail(
PhabricatorUser $user,
PhabricatorUserEmail $new) {
$username = $user->getUsername();
$old_address = $this->getAddress();
$new_address = $new->getAddress();
$body = sprintf(
"%s\n\n%s\n",
pht('Hi %s', $username),
pht(
'This email address (%s) is no longer your primary email address. '.
'Going forward, Phabricator will send all email to your new primary '.
'email address (%s).',
$old_address,
$new_address));
id(new PhabricatorMetaMTAMail())
->addRawTos(array($old_address))
->setForceDelivery(true)
->setSubject(pht('[Phabricator] Primary Address Changed'))
->setBody($body)
->setFrom($user->getPHID())
->setRelatedPHID($user->getPHID())
->saveAndSend();
}
/**
* Send a notification email from $user to this address, informing the
* recipient that this is now their account's new primary email address.
*
* @param PhabricatorUser The user sending the verification.
* @return this
* @task email
*/
public function sendNewPrimaryEmail(PhabricatorUser $user) {
$username = $user->getUsername();
$new_address = $this->getAddress();
$body = sprintf(
"%s\n\n%s\n",
pht('Hi %s', $username),
pht(
'This is now your primary email address (%s). Going forward, '.
'Phabricator will send all email here.',
$new_address));
id(new PhabricatorMetaMTAMail())
->addRawTos(array($new_address))
->setForceDelivery(true)
->setSubject(pht('[Phabricator] Primary Address Changed'))
->setBody($body)
->setFrom($user->getPHID())
->setRelatedPHID($user->getPHID())
->saveAndSend();
return $this;
}
}
diff --git a/src/applications/people/storage/PhabricatorUserTransaction.php b/src/applications/people/storage/PhabricatorUserTransaction.php
index 24edb2f5b..01d94b9fa 100644
--- a/src/applications/people/storage/PhabricatorUserTransaction.php
+++ b/src/applications/people/storage/PhabricatorUserTransaction.php
@@ -1,22 +1,22 @@
<?php
final class PhabricatorUserTransaction
extends PhabricatorModularTransaction {
public function getApplicationName() {
return 'user';
}
public function getApplicationTransactionType() {
return PhabricatorPeopleUserPHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return null;
+ public function getBaseTransactionClass() {
+ return 'PhabricatorUserTransactionType';
}
public function getBaseTransactionClass() {
return 'PhabricatorUserTransactionType';
}
}
diff --git a/src/applications/people/typeahead/PhabricatorPeopleDatasource.php b/src/applications/people/typeahead/PhabricatorPeopleDatasource.php
index df146808b..d4a5ad96c 100644
--- a/src/applications/people/typeahead/PhabricatorPeopleDatasource.php
+++ b/src/applications/people/typeahead/PhabricatorPeopleDatasource.php
@@ -1,105 +1,114 @@
<?php
final class PhabricatorPeopleDatasource
extends PhabricatorTypeaheadDatasource {
public function getBrowseTitle() {
return pht('Browse Users');
}
public function getPlaceholderText() {
return pht('Type a username...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorPeopleApplication';
}
public function loadResults() {
$viewer = $this->getViewer();
$query = id(new PhabricatorPeopleQuery())
- ->setOrderVector(array('username'));
+ ->setOrderVector(array('username'))
+ ->needAvailability(true);
if ($this->getPhase() == self::PHASE_PREFIX) {
$prefix = $this->getPrefixQuery();
$query->withNamePrefixes(array($prefix));
} else {
$tokens = $this->getTokens();
if ($tokens) {
$query->withNameTokens($tokens);
}
}
$users = $this->executeQuery($query);
$is_browse = $this->getIsBrowse();
if ($is_browse && $users) {
$phids = mpull($users, 'getPHID');
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs($phids)
->execute();
}
$results = array();
foreach ($users as $user) {
$phid = $user->getPHID();
$closed = null;
if ($user->getIsDisabled()) {
$closed = pht('Disabled');
} else if ($user->getIsSystemAgent()) {
$closed = pht('Bot');
} else if ($user->getIsMailingList()) {
$closed = pht('Mailing List');
}
$username = $user->getUsername();
$result = id(new PhabricatorTypeaheadResult())
->setName($user->getFullName())
->setURI('/p/'.$username.'/')
->setPHID($phid)
->setPriorityString($username)
->setPriorityType('user')
->setAutocomplete('@'.$username)
->setClosed($closed);
if ($user->getIsMailingList()) {
$result->setIcon('fa-envelope-o');
}
if ($is_browse) {
$handle = $handles[$phid];
$result
->setIcon($handle->getIcon())
->setImageURI($handle->getImageURI())
->addAttribute($handle->getSubtitle());
if ($user->getIsAdmin()) {
$result->addAttribute(
array(
id(new PHUIIconView())->setIcon('fa-star'),
' ',
pht('Administrator'),
));
}
if ($user->getIsAdmin()) {
$display_type = pht('Administrator');
} else {
$display_type = pht('User');
}
$result->setDisplayType($display_type);
}
+ $until = $user->getAwayUntil();
+ if ($until) {
+ $availability = $user->getDisplayAvailability();
+ $color = PhabricatorCalendarEventInvitee::getAvailabilityColor(
+ $availability);
+ $result->setAvailabilityColor($color);
+ }
+
$results[] = $result;
}
return $results;
}
}
diff --git a/src/applications/people/view/PhabricatorUserCardView.php b/src/applications/people/view/PhabricatorUserCardView.php
index f1fc515f8..21cb468ba 100644
--- a/src/applications/people/view/PhabricatorUserCardView.php
+++ b/src/applications/people/view/PhabricatorUserCardView.php
@@ -1,173 +1,176 @@
<?php
final class PhabricatorUserCardView extends AphrontTagView {
private $profile;
private $viewer;
private $tag;
public function setProfile(PhabricatorUser $profile) {
$this->profile = $profile;
return $this;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function setTag($tag) {
$this->tag = $tag;
return $this;
}
protected function getTagName() {
if ($this->tag) {
return $this->tag;
}
return 'div';
}
protected function getTagAttributes() {
$classes = array();
$classes[] = 'project-card-view';
$classes[] = 'people-card-view';
if ($this->profile->getIsDisabled()) {
$classes[] = 'project-card-disabled';
}
return array(
'class' => implode($classes, ' '),
);
}
protected function getTagContent() {
$user = $this->profile;
$profile = $user->loadUserProfile();
$picture = $user->getProfileImageURI();
$viewer = $this->viewer;
require_celerity_resource('project-card-view-css');
// We don't have a ton of room on the hovercard, so we're trying to show
// the most important tag. Users can click through to the profile to get
// more details.
$classes = array();
if ($user->getIsDisabled()) {
$tag_icon = 'fa-ban';
$tag_title = pht('Disabled');
$tag_shade = PHUITagView::COLOR_RED;
$classes[] = 'phui-image-disabled';
} else if (!$user->getIsApproved()) {
$tag_icon = 'fa-ban';
$tag_title = pht('Unapproved Account');
$tag_shade = PHUITagView::COLOR_RED;
} else if (!$user->getIsEmailVerified()) {
$tag_icon = 'fa-envelope';
$tag_title = pht('Email Not Verified');
$tag_shade = PHUITagView::COLOR_VIOLET;
} else if ($user->getIsAdmin()) {
$tag_icon = 'fa-star';
$tag_title = pht('Administrator');
$tag_shade = PHUITagView::COLOR_INDIGO;
} else {
$tag_icon = PhabricatorPeopleIconSet::getIconIcon($profile->getIcon());
$tag_title = $profile->getDisplayTitle();
$tag_shade = null;
}
$tag = id(new PHUITagView())
->setIcon($tag_icon)
->setName($tag_title)
->setType(PHUITagView::TYPE_SHADE);
if ($tag_shade !== null) {
$tag->setColor($tag_shade);
}
$body = array();
/* TODO: Replace with Conpherence Availability if we ship it */
$body[] = $this->addItem(
'fa-user-plus',
phabricator_date($user->getDateCreated(), $viewer));
- if (PhabricatorApplication::isClassInstalledForViewer(
- 'PhabricatorCalendarApplication',
- $viewer)) {
- $body[] = $this->addItem(
- 'fa-calendar-o',
- id(new PHUIUserAvailabilityView())
- ->setViewer($viewer)
- ->setAvailableUser($user));
+ $has_calendar = PhabricatorApplication::isClassInstalledForViewer(
+ 'PhabricatorCalendarApplication',
+ $viewer);
+ if ($has_calendar) {
+ if (!$user->getIsDisabled()) {
+ $body[] = $this->addItem(
+ 'fa-calendar-o',
+ id(new PHUIUserAvailabilityView())
+ ->setViewer($viewer)
+ ->setAvailableUser($user));
+ }
}
$classes[] = 'project-card-image';
$image = phutil_tag(
'img',
array(
'src' => $picture,
'class' => implode(' ', $classes),
));
$href = urisprintf(
'/p/%s/',
$user->getUsername());
$image = phutil_tag(
'a',
array(
'href' => $href,
'class' => 'project-card-image-href',
),
$image);
$name = phutil_tag_div('project-card-name',
$user->getRealname());
$username = phutil_tag_div('project-card-username',
'@'.$user->getUsername());
$tag = phutil_tag_div('phui-header-subheader',
$tag);
$header = phutil_tag(
'div',
array(
'class' => 'project-card-header',
),
array(
$name,
$username,
$tag,
$body,
));
$card = phutil_tag(
'div',
array(
'class' => 'project-card-inner',
),
array(
- $image,
$header,
+ $image,
));
return $card;
}
private function addItem($icon, $value) {
$icon = id(new PHUIIconView())
->addClass('project-card-item-icon')
->setIcon($icon);
$text = phutil_tag(
'span',
array(
'class' => 'project-card-item-text',
),
$value);
return phutil_tag_div('project-card-item', array($icon, $text));
}
}
diff --git a/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php b/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php
index b6d23b351..b436b7671 100644
--- a/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php
+++ b/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php
@@ -1,99 +1,117 @@
<?php
final class PhabricatorUserUsernameTransaction
extends PhabricatorUserTransactionType {
const TRANSACTIONTYPE = 'user.rename';
public function generateOldValue($object) {
return $object->getUsername();
}
public function generateNewValue($object, $value) {
return $value;
}
public function applyInternalEffects($object, $value) {
$object->setUsername($value);
}
public function applyExternalEffects($object, $value) {
+ $actor = $this->getActor();
$user = $object;
+ $old_username = $this->getOldValue();
+ $new_username = $this->getNewValue();
+
$this->newUserLog(PhabricatorUserLog::ACTION_CHANGE_USERNAME)
- ->setOldValue($this->getOldValue())
- ->setNewValue($value)
+ ->setOldValue($old_username)
+ ->setNewValue($new_username)
->save();
// The SSH key cache currently includes usernames, so dirty it. See T12554
// for discussion.
PhabricatorAuthSSHKeyQuery::deleteSSHKeyCache();
- $user->sendUsernameChangeEmail($this->getActor(), $this->getOldValue());
+ id(new PhabricatorPeopleUsernameMailEngine())
+ ->setSender($actor)
+ ->setRecipient($object)
+ ->setOldUsername($old_username)
+ ->setNewUsername($new_username)
+ ->sendMail();
}
public function getTitle() {
return pht(
'%s renamed this user 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) {
$actor = $this->getActor();
$errors = array();
foreach ($xactions as $xaction) {
$new = $xaction->getNewValue();
$old = $xaction->getOldValue();
if ($old === $new) {
continue;
}
if (!$actor->getIsAdmin()) {
$errors[] = $this->newInvalidError(
pht('You must be an administrator to rename users.'));
}
if (!strlen($new)) {
$errors[] = $this->newRequiredError(
pht('New username is required.'), $xaction);
} else if (!PhabricatorUser::validateUsername($new)) {
$errors[] = $this->newInvalidError(
PhabricatorUser::describeValidUsername(), $xaction);
}
$user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUsernames(array($new))
->executeOne();
if ($user) {
$errors[] = $this->newInvalidError(
pht('Another user already has that username.'), $xaction);
}
}
return $errors;
}
public function getRequiredCapabilities(
$object,
PhabricatorApplicationTransaction $xaction) {
// Unlike normal user edits, renames require admin permissions, which
// is enforced by validateTransactions().
return null;
}
public function shouldTryMFA(
$object,
PhabricatorApplicationTransaction $xaction) {
return true;
}
}
diff --git a/src/applications/phame/controller/post/PhamePostViewController.php b/src/applications/phame/controller/post/PhamePostViewController.php
index 11d94d2f9..4fb01c4de 100644
--- a/src/applications/phame/controller/post/PhamePostViewController.php
+++ b/src/applications/phame/controller/post/PhamePostViewController.php
@@ -1,353 +1,360 @@
<?php
final class PhamePostViewController
extends PhameLiveController {
public function handleRequest(AphrontRequest $request) {
$response = $this->setupLiveEnvironment();
if ($response) {
return $response;
}
$viewer = $request->getViewer();
$moved = $request->getStr('moved');
$post = $this->getPost();
$blog = $this->getBlog();
$is_live = $this->getIsLive();
$is_external = $this->getIsExternal();
$header = id(new PHUIHeaderView())
->addClass('phame-header-bar')
->setUser($viewer);
$hero = $this->buildPhamePostHeader($post);
if (!$is_external) {
$actions = $this->renderActions($post);
$header->setPolicyObject($post);
$header->setActionList($actions);
}
$document = id(new PHUIDocumentView())
->setHeader($header);
if ($moved) {
$document->appendChild(
id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->appendChild(pht('Post moved successfully.')));
}
if ($post->isDraft()) {
$document->appendChild(
id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setTitle(pht('Draft Post'))
->appendChild(
pht(
'This is a draft, and is only visible to you and other users '.
'who can edit %s. Use "Publish" to publish this post.',
$viewer->renderHandle($post->getBlogPHID()))));
}
if ($post->isArchived()) {
$document->appendChild(
id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_ERROR)
->setTitle(pht('Archived Post'))
->appendChild(
pht(
'This post has been archived, and is only visible to you and '.
'other users who can edit %s.',
$viewer->renderHandle($post->getBlogPHID()))));
}
if (!$post->getBlog()) {
$document->appendChild(
id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setTitle(pht('Not On A Blog'))
->appendChild(
pht('This post is not associated with a blog (the blog may have '.
'been deleted). Use "Move Post" to move it to a new blog.')));
}
$engine = id(new PhabricatorMarkupEngine())
->setViewer($viewer)
->addObject($post, PhamePost::MARKUP_FIELD_BODY)
->process();
$document->appendChild(
phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$engine->getOutput($post, PhamePost::MARKUP_FIELD_BODY)));
$blogger = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs(array($post->getBloggerPHID()))
->needProfileImage(true)
->executeOne();
$blogger_profile = $blogger->loadUserProfile();
$author_uri = '/p/'.$blogger->getUsername().'/';
$author_uri = PhabricatorEnv::getURI($author_uri);
$author = phutil_tag(
'a',
array(
'href' => $author_uri,
),
$blogger->getUsername());
$date = phabricator_datetime($post->getDatePublished(), $viewer);
if ($post->isDraft()) {
$subtitle = pht('Unpublished draft by %s.', $author);
} else if ($post->isArchived()) {
$subtitle = pht('Archived post by %s.', $author);
} else {
$subtitle = pht('Written by %s on %s.', $author, $date);
}
$user_icon = $blogger_profile->getIcon();
$user_icon = PhabricatorPeopleIconSet::getIconIcon($user_icon);
$user_icon = id(new PHUIIconView())->setIcon($user_icon);
$about = id(new PhameDescriptionView())
->setTitle($subtitle)
->setDescription(
array(
$user_icon,
' ',
$blogger_profile->getDisplayTitle(),
))
->setImage($blogger->getProfileImageURI())
->setImageHref($author_uri);
$monogram = $post->getMonogram();
$timeline = $this->buildTransactionTimeline(
$post,
id(new PhamePostTransactionQuery())
->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT)));
$timeline->setQuoteRef($monogram);
if ($is_external) {
$add_comment = null;
} else {
$add_comment = $this->buildCommentForm($post, $timeline);
$add_comment = phutil_tag_div('mlb mlt phame-comment-view', $add_comment);
}
$timeline = phutil_tag_div('phui-document-view-pro-box', $timeline);
list($prev, $next) = $this->loadAdjacentPosts($post);
$properties = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($post);
$is_live = $this->getIsLive();
$is_external = $this->getIsExternal();
$next_view = new PhameNextPostView();
if ($next) {
$next_view->setNext($next->getTitle(),
$next->getBestURI($is_live, $is_external));
}
if ($prev) {
$next_view->setPrevious($prev->getTitle(),
$prev->getBestURI($is_live, $is_external));
}
$document->setFoot($next_view);
$crumbs = $this->buildApplicationCrumbs();
$properties = phutil_tag_div('phui-document-view-pro-box', $properties);
$page = $this->newPage()
->setTitle($post->getTitle())
->setPageObjectPHIDs(array($post->getPHID()))
->setCrumbs($crumbs)
->appendChild(
array(
$hero,
$document,
$about,
$properties,
$timeline,
$add_comment,
));
if ($is_live) {
$page
->setShowChrome(false)
->setShowFooter(false);
}
return $page;
}
private function renderActions(PhamePost $post) {
$viewer = $this->getViewer();
$actions = id(new PhabricatorActionListView())
->setObject($post)
->setUser($viewer);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$post,
PhabricatorPolicyCapability::CAN_EDIT);
$id = $post->getID();
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setHref($this->getApplicationURI('post/edit/'.$id.'/'))
->setName(pht('Edit Post'))
->setDisabled(!$can_edit));
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-camera-retro')
->setHref($this->getApplicationURI('post/header/'.$id.'/'))
->setName(pht('Edit Header Image'))
->setDisabled(!$can_edit));
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-arrows')
->setHref($this->getApplicationURI('post/move/'.$id.'/'))
->setName(pht('Move Post'))
->setDisabled(!$can_edit)
->setWorkflow(true));
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-history')
->setHref($this->getApplicationURI('post/history/'.$id.'/'))
->setName(pht('View History')));
if ($post->isDraft()) {
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-eye')
->setHref($this->getApplicationURI('post/publish/'.$id.'/'))
->setName(pht('Publish'))
->setDisabled(!$can_edit)
->setWorkflow(true));
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-ban')
->setHref($this->getApplicationURI('post/archive/'.$id.'/'))
->setName(pht('Archive'))
->setDisabled(!$can_edit)
->setWorkflow(true));
} else if ($post->isArchived()) {
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-eye')
->setHref($this->getApplicationURI('post/publish/'.$id.'/'))
->setName(pht('Publish'))
->setDisabled(!$can_edit)
->setWorkflow(true));
} else {
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-eye-slash')
->setHref($this->getApplicationURI('post/unpublish/'.$id.'/'))
->setName(pht('Unpublish'))
->setDisabled(!$can_edit)
->setWorkflow(true));
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-ban')
->setHref($this->getApplicationURI('post/archive/'.$id.'/'))
->setName(pht('Archive'))
->setDisabled(!$can_edit)
->setWorkflow(true));
}
if ($post->isDraft()) {
$live_name = pht('Preview');
} else {
$live_name = pht('View Live');
}
if (!$post->isArchived()) {
$actions->addAction(
id(new PhabricatorActionView())
->setUser($viewer)
->setIcon('fa-globe')
->setHref($post->getLiveURI())
->setName($live_name));
}
return $actions;
}
private function buildCommentForm(PhamePost $post, $timeline) {
$viewer = $this->getViewer();
$box = id(new PhamePostEditEngine())
->setViewer($viewer)
->buildEditEngineCommentView($post)
->setTransactionTimeline($timeline);
return phutil_tag_div('phui-document-view-pro-box', $box);
}
private function loadAdjacentPosts(PhamePost $post) {
$viewer = $this->getViewer();
+ $pager = id(new AphrontCursorPagerView())
+ ->setPageSize(1);
+
+ $prev_pager = id(clone $pager)
+ ->setAfterID($post->getID());
+
+ $next_pager = id(clone $pager)
+ ->setBeforeID($post->getID());
+
$query = id(new PhamePostQuery())
->setViewer($viewer)
->withVisibility(array(PhameConstants::VISIBILITY_PUBLISHED))
->withBlogPHIDs(array($post->getBlog()->getPHID()))
->setLimit(1);
$prev = id(clone $query)
- ->setAfterID($post->getID())
- ->execute();
+ ->executeWithCursorPager($prev_pager);
$next = id(clone $query)
- ->setBeforeID($post->getID())
- ->execute();
+ ->executeWithCursorPager($next_pager);
return array(head($prev), head($next));
}
private function buildPhamePostHeader(
PhamePost $post) {
$image = null;
if ($post->getHeaderImagePHID()) {
$image = phutil_tag(
'div',
array(
'class' => 'phame-header-hero',
),
phutil_tag(
'img',
array(
'src' => $post->getHeaderImageURI(),
'class' => 'phame-header-image',
)));
}
$title = phutil_tag_div('phame-header-title', $post->getTitle());
$subtitle = null;
if ($post->getSubtitle()) {
$subtitle = phutil_tag_div('phame-header-subtitle', $post->getSubtitle());
}
return phutil_tag_div(
'phame-mega-header', array($image, $title, $subtitle));
}
}
diff --git a/src/applications/phame/query/PhamePostQuery.php b/src/applications/phame/query/PhamePostQuery.php
index 85ef470ce..d7396e553 100644
--- a/src/applications/phame/query/PhamePostQuery.php
+++ b/src/applications/phame/query/PhamePostQuery.php
@@ -1,194 +1,190 @@
<?php
final class PhamePostQuery extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $blogPHIDs;
private $bloggerPHIDs;
private $visibility;
private $publishedAfter;
private $phids;
private $needHeaderImage;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withBloggerPHIDs(array $blogger_phids) {
$this->bloggerPHIDs = $blogger_phids;
return $this;
}
public function withBlogPHIDs(array $blog_phids) {
$this->blogPHIDs = $blog_phids;
return $this;
}
public function withVisibility(array $visibility) {
$this->visibility = $visibility;
return $this;
}
public function withPublishedAfter($time) {
$this->publishedAfter = $time;
return $this;
}
public function needHeaderImage($need) {
$this->needHeaderImage = $need;
return $this;
}
public function newResultObject() {
return new PhamePost();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $posts) {
// We require blogs to do visibility checks, so load them unconditionally.
$blog_phids = mpull($posts, 'getBlogPHID');
$blogs = id(new PhameBlogQuery())
->setViewer($this->getViewer())
->needProfileImage(true)
->withPHIDs($blog_phids)
->execute();
$blogs = mpull($blogs, null, 'getPHID');
foreach ($posts as $key => $post) {
$blog_phid = $post->getBlogPHID();
$blog = idx($blogs, $blog_phid);
if (!$blog) {
$this->didRejectResult($post);
unset($posts[$key]);
continue;
}
$post->attachBlog($blog);
}
if ($this->needHeaderImage) {
$file_phids = mpull($posts, 'getHeaderImagePHID');
$file_phids = array_filter($file_phids);
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
} else {
$files = array();
}
foreach ($posts as $post) {
$file = idx($files, $post->getHeaderImagePHID());
if ($file) {
$post->attachHeaderImageFile($file);
}
}
}
return $posts;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'p.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'p.phid IN (%Ls)',
$this->phids);
}
if ($this->bloggerPHIDs !== null) {
$where[] = qsprintf(
$conn,
'p.bloggerPHID IN (%Ls)',
$this->bloggerPHIDs);
}
if ($this->visibility !== null) {
$where[] = qsprintf(
$conn,
'p.visibility IN (%Ld)',
$this->visibility);
}
if ($this->publishedAfter !== null) {
$where[] = qsprintf(
$conn,
'p.datePublished > %d',
$this->publishedAfter);
}
if ($this->blogPHIDs !== null) {
$where[] = qsprintf(
$conn,
'p.blogPHID in (%Ls)',
$this->blogPHIDs);
}
return $where;
}
public function getBuiltinOrders() {
return array(
'datePublished' => array(
'vector' => array('datePublished', 'id'),
'name' => pht('Publish Date'),
),
) + parent::getBuiltinOrders();
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'datePublished' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'datePublished',
'type' => 'int',
'reverse' => false,
),
);
}
- protected function getPagingValueMap($cursor, array $keys) {
- $post = $this->loadCursorObject($cursor);
-
- $map = array(
- 'datePublished' => $post->getDatePublished(),
- 'id' => $post->getID(),
+ protected function newPagingMapFromPartialObject($object) {
+ return array(
+ 'id' => (int)$object->getID(),
+ 'datePublished' => (int)$object->getDatePublished(),
);
-
- return $map;
}
public function getQueryApplicationClass() {
// TODO: Does setting this break public blogs?
return null;
}
protected function getPrimaryTableAlias() {
return 'p';
}
}
diff --git a/src/applications/phame/storage/PhameBlogTransaction.php b/src/applications/phame/storage/PhameBlogTransaction.php
index d3d6a79d0..c605510d7 100644
--- a/src/applications/phame/storage/PhameBlogTransaction.php
+++ b/src/applications/phame/storage/PhameBlogTransaction.php
@@ -1,50 +1,46 @@
<?php
final class PhameBlogTransaction
extends PhabricatorModularTransaction {
const MAILTAG_DETAILS = 'phame-blog-details';
const MAILTAG_SUBSCRIBERS = 'phame-blog-subscribers';
const MAILTAG_OTHER = 'phame-blog-other';
public function getApplicationName() {
return 'phame';
}
public function getApplicationTransactionType() {
return PhabricatorPhameBlogPHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return null;
- }
-
public function getBaseTransactionClass() {
return 'PhameBlogTransactionType';
}
public function getMailTags() {
$tags = parent::getMailTags();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$tags[] = self::MAILTAG_SUBSCRIBERS;
break;
case PhameBlogNameTransaction::TRANSACTIONTYPE:
case PhameBlogSubtitleTransaction::TRANSACTIONTYPE:
case PhameBlogDescriptionTransaction::TRANSACTIONTYPE:
case PhameBlogFullDomainTransaction::TRANSACTIONTYPE:
case PhameBlogParentSiteTransaction::TRANSACTIONTYPE:
case PhameBlogParentDomainTransaction::TRANSACTIONTYPE:
case PhameBlogProfileImageTransaction::TRANSACTIONTYPE:
case PhameBlogHeaderImageTransaction::TRANSACTIONTYPE:
$tags[] = self::MAILTAG_DETAILS;
break;
default:
$tags[] = self::MAILTAG_OTHER;
break;
}
return $tags;
}
}
diff --git a/src/applications/phlux/query/PhluxVariableQuery.php b/src/applications/phlux/query/PhluxVariableQuery.php
index 75abd044d..8ec4bc933 100644
--- a/src/applications/phlux/query/PhluxVariableQuery.php
+++ b/src/applications/phlux/query/PhluxVariableQuery.php
@@ -1,95 +1,95 @@
<?php
final class PhluxVariableQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $keys;
private $phids;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withKeys(array $keys) {
$this->keys = $keys;
return $this;
}
protected function loadPage() {
$table = new PhluxVariable();
$conn_r = $table->establishConnection('r');
$rows = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($rows);
}
protected function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->keys !== null) {
$where[] = qsprintf(
$conn,
'variableKey IN (%Ls)',
$this->keys);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
$where[] = $this->buildPagingClause($conn);
return $this->formatWhereClause($conn, $where);
}
protected function getDefaultOrderVector() {
return array('key');
}
public function getOrderableColumns() {
return array(
'key' => array(
'column' => 'variableKey',
'type' => 'string',
'reverse' => true,
'unique' => true,
),
);
}
- protected function getPagingValueMap($cursor, array $keys) {
- $object = $this->loadCursorObject($cursor);
+ protected function newPagingMapFromPartialObject($object) {
return array(
+ 'id' => (int)$object->getID(),
'key' => $object->getVariableKey(),
);
}
public function getQueryApplicationClass() {
return 'PhabricatorPhluxApplication';
}
}
diff --git a/src/applications/phlux/storage/PhluxTransaction.php b/src/applications/phlux/storage/PhluxTransaction.php
index 1224caf20..b1624d581 100644
--- a/src/applications/phlux/storage/PhluxTransaction.php
+++ b/src/applications/phlux/storage/PhluxTransaction.php
@@ -1,53 +1,49 @@
<?php
final class PhluxTransaction extends PhabricatorApplicationTransaction {
const TYPE_EDIT_KEY = 'phlux:key';
const TYPE_EDIT_VALUE = 'phlux:value';
public function getApplicationName() {
return 'phlux';
}
public function getApplicationTransactionType() {
return PhluxVariablePHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return null;
- }
-
public function getTitle() {
$author_phid = $this->getAuthorPHID();
switch ($this->getTransactionType()) {
case self::TYPE_EDIT_KEY:
return pht(
'%s created this variable.',
$this->renderHandleLink($author_phid));
case self::TYPE_EDIT_VALUE:
return pht(
'%s updated this variable.',
$this->renderHandleLink($author_phid));
}
return parent::getTitle();
}
public function hasChangeDetails() {
switch ($this->getTransactionType()) {
case self::TYPE_EDIT_VALUE:
return true;
}
return parent::hasChangeDetails();
}
public function renderChangeDetails(PhabricatorUser $viewer) {
return $this->renderTextCorpusChangeDetails(
$viewer,
json_encode($this->getOldValue()),
json_encode($this->getNewValue()));
}
}
diff --git a/src/applications/pholio/view/PholioMockImagesView.php b/src/applications/pholio/view/PholioMockImagesView.php
index 99645c4a9..786de07cf 100644
--- a/src/applications/pholio/view/PholioMockImagesView.php
+++ b/src/applications/pholio/view/PholioMockImagesView.php
@@ -1,223 +1,223 @@
<?php
final class PholioMockImagesView extends AphrontView {
private $mock;
private $imageID;
private $requestURI;
private $commentFormID;
private $panelID;
private $viewportID;
private $behaviorConfig;
public function setCommentFormID($comment_form_id) {
$this->commentFormID = $comment_form_id;
return $this;
}
public function getCommentFormID() {
return $this->commentFormID;
}
public function setRequestURI(PhutilURI $request_uri) {
$this->requestURI = $request_uri;
return $this;
}
public function getRequestURI() {
return $this->requestURI;
}
public function setImageID($image_id) {
$this->imageID = $image_id;
return $this;
}
public function getImageID() {
return $this->imageID;
}
public function setMock(PholioMock $mock) {
$this->mock = $mock;
return $this;
}
public function getMock() {
return $this->mock;
}
public function __construct() {
$this->panelID = celerity_generate_unique_node_id();
$this->viewportID = celerity_generate_unique_node_id();
}
public function getBehaviorConfig() {
if (!$this->getMock()) {
throw new PhutilInvalidStateException('setMock');
}
if ($this->behaviorConfig === null) {
$this->behaviorConfig = $this->calculateBehaviorConfig();
}
return $this->behaviorConfig;
}
private function calculateBehaviorConfig() {
$mock = $this->getMock();
// TODO: We could maybe do a better job with tailoring this, which is the
// image shown on the review stage.
$viewer = $this->getUser();
$default = PhabricatorFile::loadBuiltin($viewer, 'image-100x100.png');
$images = array();
$current_set = 0;
foreach ($mock->getImages() as $image) {
$file = $image->getFile();
$metadata = $file->getMetadata();
$x = idx($metadata, PhabricatorFile::METADATA_IMAGE_WIDTH);
$y = idx($metadata, PhabricatorFile::METADATA_IMAGE_HEIGHT);
$is_obs = (bool)$image->getIsObsolete();
if (!$is_obs) {
$current_set++;
}
$description = $image->getDescription();
if (strlen($description)) {
$description = new PHUIRemarkupView($viewer, $description);
}
$history_uri = '/pholio/image/history/'.$image->getID().'/';
$images[] = array(
'id' => $image->getID(),
'fullURI' => $file->getBestURI(),
'stageURI' => ($file->isViewableImage()
? $file->getBestURI()
: $default->getBestURI()),
'pageURI' => $this->getImagePageURI($image, $mock),
'downloadURI' => $file->getDownloadURI(),
'historyURI' => $history_uri,
'width' => $x,
'height' => $y,
'title' => $image->getName(),
'descriptionMarkup' => $description,
'isObsolete' => (bool)$image->getIsObsolete(),
'isImage' => $file->isViewableImage(),
'isViewable' => $file->isViewableInBrowser(),
);
}
$ids = mpull($mock->getActiveImages(), null, 'getID');
if ($this->imageID && isset($ids[$this->imageID])) {
$selected_id = $this->imageID;
} else {
$selected_id = head_key($ids);
}
$navsequence = array();
foreach ($mock->getActiveImages() as $image) {
$navsequence[] = $image->getID();
}
$full_icon = array(
javelin_tag('span', array('aural' => true), pht('View Raw File')),
id(new PHUIIconView())->setIcon('fa-file-image-o'),
);
$download_icon = array(
javelin_tag('span', array('aural' => true), pht('Download File')),
id(new PHUIIconView())->setIcon('fa-download'),
);
$login_uri = id(new PhutilURI('/login/'))
- ->setQueryParam('next', (string)$this->getRequestURI());
+ ->replaceQueryParam('next', (string)$this->getRequestURI());
$config = array(
'mockID' => $mock->getID(),
'panelID' => $this->panelID,
'viewportID' => $this->viewportID,
'commentFormID' => $this->getCommentFormID(),
'images' => $images,
'selectedID' => $selected_id,
'loggedIn' => $this->getUser()->isLoggedIn(),
'logInLink' => (string)$login_uri,
'navsequence' => $navsequence,
'fullIcon' => hsprintf('%s', $full_icon),
'downloadIcon' => hsprintf('%s', $download_icon),
'currentSetSize' => $current_set,
);
return $config;
}
public function render() {
if (!$this->getMock()) {
throw new PhutilInvalidStateException('setMock');
}
$mock = $this->getMock();
require_celerity_resource('javelin-behavior-pholio-mock-view');
$panel_id = $this->panelID;
$viewport_id = $this->viewportID;
$config = $this->getBehaviorConfig();
Javelin::initBehavior(
'pholio-mock-view',
$this->getBehaviorConfig());
$mock_wrapper = javelin_tag(
'div',
array(
'id' => $this->viewportID,
'sigil' => 'mock-viewport',
'class' => 'pholio-mock-image-viewport',
),
'');
$image_header = javelin_tag(
'div',
array(
'id' => 'mock-image-header',
'class' => 'pholio-mock-image-header',
),
'');
$mock_wrapper = javelin_tag(
'div',
array(
'id' => $this->panelID,
'sigil' => 'mock-panel touchable',
'class' => 'pholio-mock-image-panel',
),
array(
$image_header,
$mock_wrapper,
));
$inline_comments_holder = javelin_tag(
'div',
array(
'id' => 'mock-image-description',
'sigil' => 'mock-image-description',
'class' => 'mock-image-description',
),
'');
return phutil_tag(
'div',
array(
'class' => 'pholio-mock-image-container',
'id' => 'pholio-mock-image-container',
),
array($mock_wrapper, $inline_comments_holder));
}
private function getImagePageURI(PholioImage $image, PholioMock $mock) {
$uri = '/M'.$mock->getID().'/'.$image->getID().'/';
return $uri;
}
}
diff --git a/src/applications/phortune/action/PhortuneAddPaymentMethodAction.php b/src/applications/phortune/action/PhortuneAddPaymentMethodAction.php
new file mode 100644
index 000000000..09a8cd2f5
--- /dev/null
+++ b/src/applications/phortune/action/PhortuneAddPaymentMethodAction.php
@@ -0,0 +1,22 @@
+<?php
+
+final class PhortuneAddPaymentMethodAction
+ extends PhabricatorSystemAction {
+
+ const TYPECONST = 'phortune.payment-method.add';
+
+ public function getActionConstant() {
+ return self::TYPECONST;
+ }
+
+ public function getScoreThreshold() {
+ return 60 / phutil_units('1 hour in seconds');
+ }
+
+ public function getLimitExplanation() {
+ return pht(
+ 'You are making too many attempts to add payment methods in a short '.
+ 'period of time.');
+ }
+
+}
diff --git a/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php b/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php
index dd611ecb7..874ecf63a 100644
--- a/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php
+++ b/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php
@@ -1,233 +1,233 @@
<?php
final class PhortuneCartCheckoutController
extends PhortuneCartController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$cart = id(new PhortuneCartQuery())
->setViewer($viewer)
->withIDs(array($id))
->needPurchases(true)
->executeOne();
if (!$cart) {
return new Aphront404Response();
}
$cancel_uri = $cart->getCancelURI();
$merchant = $cart->getMerchant();
switch ($cart->getStatus()) {
case PhortuneCart::STATUS_BUILDING:
return $this->newDialog()
->setTitle(pht('Incomplete Cart'))
->appendParagraph(
pht(
'The application that created this cart did not finish putting '.
'products in it. You can not checkout with an incomplete '.
'cart.'))
->addCancelButton($cancel_uri);
case PhortuneCart::STATUS_READY:
// This is the expected, normal state for a cart that's ready for
// checkout.
break;
case PhortuneCart::STATUS_CHARGED:
case PhortuneCart::STATUS_PURCHASING:
case PhortuneCart::STATUS_HOLD:
case PhortuneCart::STATUS_REVIEW:
case PhortuneCart::STATUS_PURCHASED:
// For these states, kick the user to the order page to give them
// information and options.
return id(new AphrontRedirectResponse())->setURI($cart->getDetailURI());
default:
throw new Exception(
pht(
'Unknown cart status "%s"!',
$cart->getStatus()));
}
$account = $cart->getAccount();
$account_uri = $this->getApplicationURI($account->getID().'/');
$methods = id(new PhortunePaymentMethodQuery())
->setViewer($viewer)
->withAccountPHIDs(array($account->getPHID()))
->withMerchantPHIDs(array($merchant->getPHID()))
->withStatuses(array(PhortunePaymentMethod::STATUS_ACTIVE))
->execute();
$e_method = null;
$errors = array();
if ($request->isFormPost()) {
// Require CAN_EDIT on the cart to actually make purchases.
PhabricatorPolicyFilter::requireCapability(
$viewer,
$cart,
PhabricatorPolicyCapability::CAN_EDIT);
$method_id = $request->getInt('paymentMethodID');
$method = idx($methods, $method_id);
if (!$method) {
$e_method = pht('Required');
$errors[] = pht('You must choose a payment method.');
}
if (!$errors) {
$provider = $method->buildPaymentProvider();
$charge = $cart->willApplyCharge($viewer, $provider, $method);
try {
$provider->applyCharge($method, $charge);
} catch (Exception $ex) {
$cart->didFailCharge($charge);
return $this->newDialog()
->setTitle(pht('Charge Failed'))
->appendParagraph(
pht(
'Unable to make payment: %s',
$ex->getMessage()))
->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
}
$cart->didApplyCharge($charge);
$done_uri = $cart->getCheckoutURI();
return id(new AphrontRedirectResponse())->setURI($done_uri);
}
}
$cart_table = $this->buildCartContentTable($cart);
$cart_box = id(new PHUIObjectBoxView())
->setFormErrors($errors)
->setHeaderText(pht('Cart Contents'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($cart_table);
$title = $cart->getName();
if (!$methods) {
$method_control = id(new AphrontFormStaticControl())
->setLabel(pht('Payment Method'))
->setValue(
phutil_tag('em', array(), pht('No payment methods configured.')));
} else {
$method_control = id(new AphrontFormRadioButtonControl())
->setLabel(pht('Payment Method'))
->setName('paymentMethodID')
->setValue($request->getInt('paymentMethodID'));
foreach ($methods as $method) {
$method_control->addButton(
$method->getID(),
$method->getFullDisplayName(),
$method->getDescription());
}
}
$method_control->setError($e_method);
$account_id = $account->getID();
+ $params = array(
+ 'merchantID' => $merchant->getID(),
+ 'cartID' => $cart->getID(),
+ );
+
$payment_method_uri = $this->getApplicationURI("{$account_id}/card/new/");
- $payment_method_uri = new PhutilURI($payment_method_uri);
- $payment_method_uri->setQueryParams(
- array(
- 'merchantID' => $merchant->getID(),
- 'cartID' => $cart->getID(),
- ));
+ $payment_method_uri = new PhutilURI($payment_method_uri, $params);
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild($method_control);
$add_providers = $this->loadCreatePaymentMethodProvidersForMerchant(
$merchant);
if ($add_providers) {
$new_method = javelin_tag(
'a',
array(
'class' => 'button button-grey',
'href' => $payment_method_uri,
),
pht('Add New Payment Method'));
$form->appendChild(
id(new AphrontFormMarkupControl())
->setValue($new_method));
}
if ($methods || $add_providers) {
$submit = id(new AphrontFormSubmitControl())
->setValue(pht('Submit Payment'))
->setDisabled(!$methods);
if ($cart->getCancelURI() !== null) {
$submit->addCancelButton($cart->getCancelURI());
}
$form->appendChild($submit);
}
$provider_form = null;
$pay_providers = $this->loadOneTimePaymentProvidersForMerchant($merchant);
if ($pay_providers) {
$one_time_options = array();
foreach ($pay_providers as $provider) {
$one_time_options[] = $provider->renderOneTimePaymentButton(
$account,
$cart,
$viewer);
}
$one_time_options = phutil_tag(
'div',
array(
'class' => 'phortune-payment-onetime-list',
),
$one_time_options);
$provider_form = new PHUIFormLayoutView();
$provider_form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Pay With'))
->setValue($one_time_options));
}
$payment_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Choose Payment Method'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($form)
->appendChild($provider_form);
$description_box = $this->renderCartDescription($cart);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Checkout'));
$crumbs->addTextCrumb($title);
$crumbs->setBorder(true);
$header = id(new PHUIHeaderView())
->setHeader($title)
->setHeaderIcon('fa-shopping-cart');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(
$cart_box,
$description_box,
$payment_box,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
}
diff --git a/src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php b/src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php
index 87bddd4d3..c06886263 100644
--- a/src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php
+++ b/src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php
@@ -1,280 +1,303 @@
<?php
final class PhortunePaymentMethodCreateController
extends PhortuneController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$account_id = $request->getURIData('accountID');
$account = id(new PhortuneAccountQuery())
->setViewer($viewer)
->withIDs(array($account_id))
->executeOne();
if (!$account) {
return new Aphront404Response();
}
$account_id = $account->getID();
$merchant = id(new PhortuneMerchantQuery())
->setViewer($viewer)
->withIDs(array($request->getInt('merchantID')))
->executeOne();
if (!$merchant) {
return new Aphront404Response();
}
$cart_id = $request->getInt('cartID');
$subscription_id = $request->getInt('subscriptionID');
if ($cart_id) {
$cancel_uri = $this->getApplicationURI("cart/{$cart_id}/checkout/");
} else if ($subscription_id) {
$cancel_uri = $this->getApplicationURI(
"{$account_id}/subscription/edit/{$subscription_id}/");
} else {
$cancel_uri = $this->getApplicationURI($account->getID().'/');
}
$providers = $this->loadCreatePaymentMethodProvidersForMerchant($merchant);
if (!$providers) {
throw new Exception(
pht(
'There are no payment providers enabled that can add payment '.
'methods.'));
}
if (count($providers) == 1) {
// If there's only one provider, always choose it.
$provider_id = head_key($providers);
} else {
$provider_id = $request->getInt('providerID');
if (empty($providers[$provider_id])) {
$choices = array();
foreach ($providers as $provider) {
$choices[] = $this->renderSelectProvider($provider);
}
$content = phutil_tag(
'div',
array(
'class' => 'phortune-payment-method-list',
),
$choices);
return $this->newDialog()
->setRenderDialogAsDiv(true)
->setTitle(pht('Add Payment Method'))
->appendParagraph(pht('Choose a payment method to add:'))
->appendChild($content)
->addCancelButton($cancel_uri);
}
}
$provider = $providers[$provider_id];
$errors = array();
+ $display_exception = null;
if ($request->isFormPost() && $request->getBool('isProviderForm')) {
$method = id(new PhortunePaymentMethod())
->setAccountPHID($account->getPHID())
->setAuthorPHID($viewer->getPHID())
->setMerchantPHID($merchant->getPHID())
->setProviderPHID($provider->getProviderConfig()->getPHID())
->setStatus(PhortunePaymentMethod::STATUS_ACTIVE);
+ // Limit the rate at which you can attempt to add payment methods. This
+ // is intended as a line of defense against using Phortune to validate a
+ // large list of stolen credit card numbers.
+
+ PhabricatorSystemActionEngine::willTakeAction(
+ array($viewer->getPHID()),
+ new PhortuneAddPaymentMethodAction(),
+ 1);
+
if (!$errors) {
$errors = $this->processClientErrors(
$provider,
$request->getStr('errors'));
}
if (!$errors) {
$client_token_raw = $request->getStr('token');
$client_token = null;
try {
$client_token = phutil_json_decode($client_token_raw);
} catch (PhutilJSONParserException $ex) {
$errors[] = pht(
'There was an error decoding token information submitted by the '.
'client. Expected a JSON-encoded token dictionary, received: %s.',
nonempty($client_token_raw, pht('nothing')));
}
if (!$provider->validateCreatePaymentMethodToken($client_token)) {
$errors[] = pht(
'There was an error with the payment token submitted by the '.
'client. Expected a valid dictionary, received: %s.',
$client_token_raw);
}
if (!$errors) {
- $errors = $provider->createPaymentMethodFromRequest(
- $request,
- $method,
- $client_token);
+ try {
+ $provider->createPaymentMethodFromRequest(
+ $request,
+ $method,
+ $client_token);
+ } catch (PhortuneDisplayException $exception) {
+ $display_exception = $exception;
+ } catch (Exception $ex) {
+ $errors = array(
+ pht('There was an error adding this payment method:'),
+ $ex->getMessage(),
+ );
+ }
}
}
- if (!$errors) {
+ if (!$errors && !$display_exception) {
$method->save();
// If we added this method on a cart flow, return to the cart to
// check out.
if ($cart_id) {
$next_uri = $this->getApplicationURI(
"cart/{$cart_id}/checkout/?paymentMethodID=".$method->getID());
} else if ($subscription_id) {
$next_uri = new PhutilURI($cancel_uri);
- $next_uri->setQueryParam('added', true);
+ $next_uri->replaceQueryParam('added', true);
} else {
$account_uri = $this->getApplicationURI($account->getID().'/');
$next_uri = new PhutilURI($account_uri);
$next_uri->setFragment('payment');
}
return id(new AphrontRedirectResponse())->setURI($next_uri);
} else {
- $dialog = id(new AphrontDialogView())
- ->setUser($viewer)
+ if ($display_exception) {
+ $dialog_body = $display_exception->getView();
+ } else {
+ $dialog_body = id(new PHUIInfoView())
+ ->setErrors($errors);
+ }
+
+ return $this->newDialog()
->setTitle(pht('Error Adding Payment Method'))
- ->appendChild(id(new PHUIInfoView())->setErrors($errors))
+ ->appendChild($dialog_body)
->addCancelButton($request->getRequestURI());
-
- return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
$form = $provider->renderCreatePaymentMethodForm($request, $errors);
$form
->setUser($viewer)
->setAction($request->getRequestURI())
->setWorkflow(true)
->addHiddenInput('providerID', $provider_id)
->addHiddenInput('cartID', $request->getInt('cartID'))
->addHiddenInput('subscriptionID', $request->getInt('subscriptionID'))
->addHiddenInput('isProviderForm', true)
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Add Payment Method'))
->addCancelButton($cancel_uri));
$box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Method'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setForm($form);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Add Payment Method'));
$crumbs->setBorder(true);
$header = id(new PHUIHeaderView())
->setHeader(pht('Add Payment Method'))
->setHeaderIcon('fa-plus-square');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(
$box,
));
return $this->newPage()
->setTitle($provider->getPaymentMethodDescription())
->setCrumbs($crumbs)
->appendChild($view);
}
private function renderSelectProvider(
PhortunePaymentProvider $provider) {
$request = $this->getRequest();
$viewer = $request->getUser();
$description = $provider->getPaymentMethodDescription();
$icon_uri = $provider->getPaymentMethodIcon();
$details = $provider->getPaymentMethodProviderDescription();
$this->requireResource('phortune-css');
$icon = id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
->setSpriteIcon($provider->getPaymentMethodIcon());
$button = id(new PHUIButtonView())
->setSize(PHUIButtonView::BIG)
->setColor(PHUIButtonView::GREY)
->setIcon($icon)
->setText($description)
->setSubtext($details)
->setMetadata(array('disableWorkflow' => true));
$form = id(new AphrontFormView())
->setUser($viewer)
->setAction($request->getRequestURI())
->addHiddenInput('providerID', $provider->getProviderConfig()->getID())
->appendChild($button);
return $form;
}
private function processClientErrors(
PhortunePaymentProvider $provider,
$client_errors_raw) {
$errors = array();
$client_errors = null;
try {
$client_errors = phutil_json_decode($client_errors_raw);
} catch (PhutilJSONParserException $ex) {
$errors[] = pht(
'There was an error decoding error information submitted by the '.
'client. Expected a JSON-encoded list of error codes, received: %s.',
nonempty($client_errors_raw, pht('nothing')));
}
foreach (array_unique($client_errors) as $key => $client_error) {
$client_errors[$key] = $provider->translateCreatePaymentMethodErrorCode(
$client_error);
}
foreach (array_unique($client_errors) as $client_error) {
switch ($client_error) {
case PhortuneErrCode::ERR_CC_INVALID_NUMBER:
$message = pht(
'The card number you entered is not a valid card number. Check '.
'that you entered it correctly.');
break;
case PhortuneErrCode::ERR_CC_INVALID_CVC:
$message = pht(
'The CVC code you entered is not a valid CVC code. Check that '.
'you entered it correctly. The CVC code is a 3-digit or 4-digit '.
'numeric code which usually appears on the back of the card.');
break;
case PhortuneErrCode::ERR_CC_INVALID_EXPIRY:
$message = pht(
'The card expiration date is not a valid expiration date. Check '.
'that you entered it correctly. You can not add an expired card '.
'as a payment method.');
break;
default:
$message = $provider->getCreatePaymentMethodErrorMessage(
$client_error);
if (!$message) {
$message = pht(
"There was an unexpected error ('%s') processing payment ".
"information.",
$client_error);
phlog($message);
}
break;
}
$errors[$client_error] = $message;
}
return $errors;
}
}
diff --git a/src/applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php b/src/applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php
index e7287f3d2..04367a88a 100644
--- a/src/applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php
+++ b/src/applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php
@@ -1,185 +1,185 @@
<?php
final class PhortuneSubscriptionEditController extends PhortuneController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$added = $request->getBool('added');
$subscription = id(new PhortuneSubscriptionQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$subscription) {
return new Aphront404Response();
}
id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$subscription->getURI());
$merchant = $subscription->getMerchant();
$account = $subscription->getAccount();
$title = pht('Subscription: %s', $subscription->getSubscriptionName());
$header = id(new PHUIHeaderView())
->setHeader($subscription->getSubscriptionName());
$view_uri = $subscription->getURI();
$valid_methods = id(new PhortunePaymentMethodQuery())
->setViewer($viewer)
->withAccountPHIDs(array($account->getPHID()))
->withStatuses(
array(
PhortunePaymentMethod::STATUS_ACTIVE,
))
->withMerchantPHIDs(array($merchant->getPHID()))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->execute();
$valid_methods = mpull($valid_methods, null, 'getPHID');
$current_phid = $subscription->getDefaultPaymentMethodPHID();
$e_method = null;
if ($current_phid && empty($valid_methods[$current_phid])) {
$e_method = pht('Needs Update');
}
$errors = array();
if ($request->isFormPost()) {
$default_method_phid = $request->getStr('defaultPaymentMethodPHID');
if (!$default_method_phid) {
$default_method_phid = null;
$e_method = null;
} else if (empty($valid_methods[$default_method_phid])) {
$e_method = pht('Invalid');
if ($default_method_phid == $current_phid) {
$errors[] = pht(
'This subscription is configured to autopay with a payment method '.
'that has been deleted. Choose a valid payment method or disable '.
'autopay.');
} else {
$errors[] = pht('You must select a valid default payment method.');
}
}
// TODO: We should use transactions here, and move the validation logic
// inside the Editor.
if (!$errors) {
$subscription->setDefaultPaymentMethodPHID($default_method_phid);
$subscription->save();
return id(new AphrontRedirectResponse())
->setURI($view_uri);
}
}
// Add the option to disable autopay.
$disable_options = array(
'' => pht('(Disable Autopay)'),
);
// Don't require the user to make a valid selection if the current method
// has become invalid.
if ($current_phid && empty($valid_methods[$current_phid])) {
$current_options = array(
$current_phid => pht('<Deleted Payment Method>'),
);
} else {
$current_options = array();
}
// Add any available options.
$valid_options = mpull($valid_methods, 'getFullDisplayName', 'getPHID');
$options = $disable_options + $current_options + $valid_options;
$crumbs = $this->buildApplicationCrumbs();
$this->addAccountCrumb($crumbs, $account);
$crumbs->addTextCrumb(
pht('Subscription %d', $subscription->getID()),
$view_uri);
$crumbs->addTextCrumb(pht('Edit'));
$crumbs->setBorder(true);
$uri = $this->getApplicationURI($account->getID().'/card/new/');
$uri = new PhutilURI($uri);
- $uri->setQueryParam('merchantID', $merchant->getID());
- $uri->setQueryParam('subscriptionID', $subscription->getID());
+ $uri->replaceQueryParam('merchantID', $merchant->getID());
+ $uri->replaceQueryParam('subscriptionID', $subscription->getID());
$add_method_button = phutil_tag(
'a',
array(
'href' => $uri,
'class' => 'button button-grey',
),
pht('Add Payment Method...'));
$radio = id(new AphrontFormRadioButtonControl())
->setName('defaultPaymentMethodPHID')
->setLabel(pht('Autopay With'))
->setValue($current_phid)
->setError($e_method);
foreach ($options as $key => $value) {
$radio->addButton($key, $value, null);
}
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild($radio)
->appendChild(
id(new AphrontFormMarkupControl())
->setValue($add_method_button))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save Changes'))
->addCancelButton($view_uri));
$box = id(new PHUIObjectBoxView())
->setUser($viewer)
->setHeaderText(pht('Subscription'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setFormErrors($errors)
->appendChild($form);
if ($added) {
$info_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_SUCCESS)
->appendChild(pht('Payment method has been successfully added.'));
$box->setInfoView($info_view);
}
$header = id(new PHUIHeaderView())
->setHeader(pht('Edit %s', $subscription->getSubscriptionName()))
->setHeaderIcon('fa-pencil');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(
$box,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
}
diff --git a/src/applications/phortune/exception/PhortuneDisplayException.php b/src/applications/phortune/exception/PhortuneDisplayException.php
new file mode 100644
index 000000000..7b2bbf687
--- /dev/null
+++ b/src/applications/phortune/exception/PhortuneDisplayException.php
@@ -0,0 +1,15 @@
+<?php
+
+final class PhortuneDisplayException
+ extends Exception {
+
+ public function setView($view) {
+ $this->view = $view;
+ return $this;
+ }
+
+ public function getView() {
+ return $this->view;
+ }
+
+}
diff --git a/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php b/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php
index 078141f8a..262606ca6 100644
--- a/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php
+++ b/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php
@@ -1,505 +1,507 @@
<?php
final class PhortunePayPalPaymentProvider extends PhortunePaymentProvider {
const PAYPAL_API_USERNAME = 'paypal.api-username';
const PAYPAL_API_PASSWORD = 'paypal.api-password';
const PAYPAL_API_SIGNATURE = 'paypal.api-signature';
const PAYPAL_MODE = 'paypal.mode';
public function isAcceptingLivePayments() {
$mode = $this->getProviderConfig()->getMetadataValue(self::PAYPAL_MODE);
return ($mode === 'live');
}
public function getName() {
return pht('PayPal');
}
public function getConfigureName() {
return pht('Add PayPal Payments Account');
}
public function getConfigureDescription() {
return pht(
'Allows you to accept various payment instruments with a paypal.com '.
'account.');
}
public function getConfigureProvidesDescription() {
return pht('This merchant accepts payments via PayPal.');
}
public function getConfigureInstructions() {
return pht(
"To configure PayPal, register or log into an existing account on ".
"[[https://paypal.com | paypal.com]] (for live payments) or ".
"[[https://sandbox.paypal.com | sandbox.paypal.com]] (for test ".
"payments). Once logged in:\n\n".
" - Navigate to {nav Tools > API Access}.\n".
" - Choose **View API Signature**.\n".
" - Copy the **API Username**, **API Password** and **Signature** ".
" into the fields above.\n\n".
"You can select whether the provider operates in test mode or ".
"accepts live payments using the **Mode** dropdown above.\n\n".
"You can either use `sandbox.paypal.com` to retrieve live credentials, ".
"or `paypal.com` to retrieve live credentials.");
}
public function getAllConfigurableProperties() {
return array(
self::PAYPAL_API_USERNAME,
self::PAYPAL_API_PASSWORD,
self::PAYPAL_API_SIGNATURE,
self::PAYPAL_MODE,
);
}
public function getAllConfigurableSecretProperties() {
return array(
self::PAYPAL_API_PASSWORD,
self::PAYPAL_API_SIGNATURE,
);
}
public function processEditForm(
AphrontRequest $request,
array $values) {
$errors = array();
$issues = array();
if (!strlen($values[self::PAYPAL_API_USERNAME])) {
$errors[] = pht('PayPal API Username is required.');
$issues[self::PAYPAL_API_USERNAME] = pht('Required');
}
if (!strlen($values[self::PAYPAL_API_PASSWORD])) {
$errors[] = pht('PayPal API Password is required.');
$issues[self::PAYPAL_API_PASSWORD] = pht('Required');
}
if (!strlen($values[self::PAYPAL_API_SIGNATURE])) {
$errors[] = pht('PayPal API Signature is required.');
$issues[self::PAYPAL_API_SIGNATURE] = pht('Required');
}
if (!strlen($values[self::PAYPAL_MODE])) {
$errors[] = pht('Mode is required.');
$issues[self::PAYPAL_MODE] = pht('Required');
}
return array($errors, $issues, $values);
}
public function extendEditForm(
AphrontRequest $request,
AphrontFormView $form,
array $values,
array $issues) {
$form
->appendChild(
id(new AphrontFormTextControl())
->setName(self::PAYPAL_API_USERNAME)
->setValue($values[self::PAYPAL_API_USERNAME])
->setError(idx($issues, self::PAYPAL_API_USERNAME, true))
->setLabel(pht('Paypal API Username')))
->appendChild(
id(new AphrontFormTextControl())
->setName(self::PAYPAL_API_PASSWORD)
->setValue($values[self::PAYPAL_API_PASSWORD])
->setError(idx($issues, self::PAYPAL_API_PASSWORD, true))
->setLabel(pht('Paypal API Password')))
->appendChild(
id(new AphrontFormTextControl())
->setName(self::PAYPAL_API_SIGNATURE)
->setValue($values[self::PAYPAL_API_SIGNATURE])
->setError(idx($issues, self::PAYPAL_API_SIGNATURE, true))
->setLabel(pht('Paypal API Signature')))
->appendChild(
id(new AphrontFormSelectControl())
->setName(self::PAYPAL_MODE)
->setValue($values[self::PAYPAL_MODE])
->setError(idx($issues, self::PAYPAL_MODE))
->setLabel(pht('Mode'))
->setOptions(
array(
'test' => pht('Test Mode'),
'live' => pht('Live Mode'),
)));
return;
}
public function canRunConfigurationTest() {
return true;
}
public function runConfigurationTest() {
$result = $this
->newPaypalAPICall()
->setRawPayPalQuery('GetBalance', array())
->resolve();
}
public function getPaymentMethodDescription() {
return pht('Credit Card or PayPal Account');
}
public function getPaymentMethodIcon() {
return 'PayPal';
}
public function getPaymentMethodProviderDescription() {
return 'PayPal';
}
protected function executeCharge(
PhortunePaymentMethod $payment_method,
PhortuneCharge $charge) {
throw new Exception('!');
}
protected function executeRefund(
PhortuneCharge $charge,
PhortuneCharge $refund) {
$transaction_id = $charge->getMetadataValue('paypal.transactionID');
if (!$transaction_id) {
throw new Exception(pht('Charge has no transaction ID!'));
}
$refund_amount = $refund->getAmountAsCurrency()->negate();
$refund_currency = $refund_amount->getCurrency();
$refund_value = $refund_amount->formatBareValue();
$params = array(
'TRANSACTIONID' => $transaction_id,
'REFUNDTYPE' => 'Partial',
'AMT' => $refund_value,
'CURRENCYCODE' => $refund_currency,
);
$result = $this
->newPaypalAPICall()
->setRawPayPalQuery('RefundTransaction', $params)
->resolve();
$charge->setMetadataValue(
'paypal.refundID',
$result['REFUNDTRANSACTIONID']);
}
public function updateCharge(PhortuneCharge $charge) {
$transaction_id = $charge->getMetadataValue('paypal.transactionID');
if (!$transaction_id) {
throw new Exception(pht('Charge has no transaction ID!'));
}
$params = array(
'TRANSACTIONID' => $transaction_id,
);
$result = $this
->newPaypalAPICall()
->setRawPayPalQuery('GetTransactionDetails', $params)
->resolve();
$is_charge = false;
$is_fail = false;
switch ($result['PAYMENTSTATUS']) {
case 'Processed':
case 'Completed':
case 'Completed-Funds-Held':
$is_charge = true;
break;
case 'Partially-Refunded':
case 'Refunded':
case 'Reversed':
case 'Canceled-Reversal':
// TODO: Handle these.
return;
case 'In-Progress':
case 'Pending':
// TODO: Also handle these better?
return;
case 'Denied':
case 'Expired':
case 'Failed':
case 'None':
case 'Voided':
default:
$is_fail = true;
break;
}
if ($charge->getStatus() == PhortuneCharge::STATUS_HOLD) {
$cart = $charge->getCart();
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
if ($is_charge) {
$cart->didApplyCharge($charge);
} else if ($is_fail) {
$cart->didFailCharge($charge);
}
unset($unguarded);
}
}
private function getPaypalAPIUsername() {
return $this
->getProviderConfig()
->getMetadataValue(self::PAYPAL_API_USERNAME);
}
private function getPaypalAPIPassword() {
return $this
->getProviderConfig()
->getMetadataValue(self::PAYPAL_API_PASSWORD);
}
private function getPaypalAPISignature() {
return $this
->getProviderConfig()
->getMetadataValue(self::PAYPAL_API_SIGNATURE);
}
/* -( One-Time Payments )-------------------------------------------------- */
public function canProcessOneTimePayments() {
return true;
}
/* -( Controllers )-------------------------------------------------------- */
public function canRespondToControllerAction($action) {
switch ($action) {
case 'checkout':
case 'charge':
case 'cancel':
return true;
}
return parent::canRespondToControllerAction();
}
public function processControllerRequest(
PhortuneProviderActionController $controller,
AphrontRequest $request) {
$viewer = $request->getUser();
$cart = $controller->loadCart($request->getInt('cartID'));
if (!$cart) {
return new Aphront404Response();
}
$charge = $controller->loadActiveCharge($cart);
switch ($controller->getAction()) {
case 'checkout':
if ($charge) {
throw new Exception(pht('Cart is already charging!'));
}
break;
case 'charge':
case 'cancel':
if (!$charge) {
throw new Exception(pht('Cart is not charging yet!'));
}
break;
}
switch ($controller->getAction()) {
case 'checkout':
$return_uri = $this->getControllerURI(
'charge',
array(
'cartID' => $cart->getID(),
));
$cancel_uri = $this->getControllerURI(
'cancel',
array(
'cartID' => $cart->getID(),
));
$price = $cart->getTotalPriceAsCurrency();
$charge = $cart->willApplyCharge($viewer, $this);
$params = array(
'PAYMENTREQUEST_0_AMT' => $price->formatBareValue(),
'PAYMENTREQUEST_0_CURRENCYCODE' => $price->getCurrency(),
'PAYMENTREQUEST_0_PAYMENTACTION' => 'Sale',
'PAYMENTREQUEST_0_CUSTOM' => $charge->getPHID(),
'PAYMENTREQUEST_0_DESC' => $cart->getName(),
'RETURNURL' => $return_uri,
'CANCELURL' => $cancel_uri,
// TODO: This should be cart-dependent if we eventually support
// physical goods.
'NOSHIPPING' => '1',
);
$result = $this
->newPaypalAPICall()
->setRawPayPalQuery('SetExpressCheckout', $params)
->resolve();
- $uri = new PhutilURI('https://www.sandbox.paypal.com/cgi-bin/webscr');
- $uri->setQueryParams(
- array(
- 'cmd' => '_express-checkout',
- 'token' => $result['TOKEN'],
- ));
+ $params = array(
+ 'cmd' => '_express-checkout',
+ 'token' => $result['TOKEN'],
+ );
+
+ $uri = new PhutilURI(
+ 'https://www.sandbox.paypal.com/cgi-bin/webscr',
+ $params);
$cart->setMetadataValue('provider.checkoutURI', (string)$uri);
$cart->save();
$charge->setMetadataValue('paypal.token', $result['TOKEN']);
$charge->save();
return id(new AphrontRedirectResponse())
->setIsExternal(true)
->setURI($uri);
case 'charge':
if ($cart->getStatus() !== PhortuneCart::STATUS_PURCHASING) {
return id(new AphrontRedirectResponse())
->setURI($cart->getCheckoutURI());
}
$token = $request->getStr('token');
$params = array(
'TOKEN' => $token,
);
$result = $this
->newPaypalAPICall()
->setRawPayPalQuery('GetExpressCheckoutDetails', $params)
->resolve();
if ($result['CUSTOM'] !== $charge->getPHID()) {
throw new Exception(
pht('Paypal checkout does not match Phortune charge!'));
}
if ($result['CHECKOUTSTATUS'] !== 'PaymentActionNotInitiated') {
return $controller->newDialog()
->setTitle(pht('Payment Already Processed'))
->appendParagraph(
pht(
'The payment response for this charge attempt has already '.
'been processed.'))
->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
}
$price = $cart->getTotalPriceAsCurrency();
$params = array(
'TOKEN' => $token,
'PAYERID' => $result['PAYERID'],
'PAYMENTREQUEST_0_AMT' => $price->formatBareValue(),
'PAYMENTREQUEST_0_CURRENCYCODE' => $price->getCurrency(),
'PAYMENTREQUEST_0_PAYMENTACTION' => 'Sale',
);
$result = $this
->newPaypalAPICall()
->setRawPayPalQuery('DoExpressCheckoutPayment', $params)
->resolve();
$transaction_id = $result['PAYMENTINFO_0_TRANSACTIONID'];
$success = false;
$hold = false;
switch ($result['PAYMENTINFO_0_PAYMENTSTATUS']) {
case 'Processed':
case 'Completed':
case 'Completed-Funds-Held':
$success = true;
break;
case 'In-Progress':
case 'Pending':
// TODO: We can capture more information about this stuff.
$hold = true;
break;
case 'Denied':
case 'Expired':
case 'Failed':
case 'Partially-Refunded':
case 'Canceled-Reversal':
case 'None':
case 'Refunded':
case 'Reversed':
case 'Voided':
default:
// These are all failure states.
break;
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$charge->setMetadataValue('paypal.transactionID', $transaction_id);
$charge->save();
if ($success) {
$cart->didApplyCharge($charge);
$response = id(new AphrontRedirectResponse())->setURI(
$cart->getCheckoutURI());
} else if ($hold) {
$cart->didHoldCharge($charge);
$response = $controller
->newDialog()
->setTitle(pht('Charge On Hold'))
->appendParagraph(
pht('Your charge is on hold, for reasons?'))
->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
} else {
$cart->didFailCharge($charge);
$response = $controller
->newDialog()
->setTitle(pht('Charge Failed'))
->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
}
unset($unguarded);
return $response;
case 'cancel':
if ($cart->getStatus() === PhortuneCart::STATUS_PURCHASING) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
// TODO: Since the user cancelled this, we could conceivably just
// throw it away or make it more clear that it's a user cancel.
$cart->didFailCharge($charge);
unset($unguarded);
}
return id(new AphrontRedirectResponse())
->setURI($cart->getCheckoutURI());
}
throw new Exception(
pht('Unsupported action "%s".', $controller->getAction()));
}
private function newPaypalAPICall() {
if ($this->isAcceptingLivePayments()) {
$host = 'https://api-3t.paypal.com/nvp';
} else {
$host = 'https://api-3t.sandbox.paypal.com/nvp';
}
return id(new PhutilPayPalAPIFuture())
->setHost($host)
->setAPIUsername($this->getPaypalAPIUsername())
->setAPIPassword($this->getPaypalAPIPassword())
->setAPISignature($this->getPaypalAPISignature());
}
}
diff --git a/src/applications/phortune/provider/PhortunePaymentProvider.php b/src/applications/phortune/provider/PhortunePaymentProvider.php
index 90e354c5d..57b2956ec 100644
--- a/src/applications/phortune/provider/PhortunePaymentProvider.php
+++ b/src/applications/phortune/provider/PhortunePaymentProvider.php
@@ -1,296 +1,295 @@
<?php
/**
* @task addmethod Adding Payment Methods
*/
abstract class PhortunePaymentProvider extends Phobject {
private $providerConfig;
public function setProviderConfig(
PhortunePaymentProviderConfig $provider_config) {
$this->providerConfig = $provider_config;
return $this;
}
public function getProviderConfig() {
return $this->providerConfig;
}
/**
* Return a short name which identifies this provider.
*/
abstract public function getName();
/* -( Configuring Providers )---------------------------------------------- */
/**
* Return a human-readable provider name for use on the merchant workflow
* where a merchant owner adds providers.
*/
abstract public function getConfigureName();
/**
* Return a human-readable provider description for use on the merchant
* workflow where a merchant owner adds providers.
*/
abstract public function getConfigureDescription();
abstract public function getConfigureInstructions();
abstract public function getConfigureProvidesDescription();
abstract public function getAllConfigurableProperties();
abstract public function getAllConfigurableSecretProperties();
/**
* Read a dictionary of properties from the provider's configuration for
* use when editing the provider.
*/
public function readEditFormValuesFromProviderConfig() {
$properties = $this->getAllConfigurableProperties();
$config = $this->getProviderConfig();
$secrets = $this->getAllConfigurableSecretProperties();
$secrets = array_fuse($secrets);
$map = array();
foreach ($properties as $property) {
$map[$property] = $config->getMetadataValue($property);
if (isset($secrets[$property])) {
$map[$property] = $this->renderConfigurationSecret($map[$property]);
}
}
return $map;
}
/**
* Read a dictionary of properties from a request for use when editing the
* provider.
*/
public function readEditFormValuesFromRequest(AphrontRequest $request) {
$properties = $this->getAllConfigurableProperties();
$map = array();
foreach ($properties as $property) {
$map[$property] = $request->getStr($property);
}
return $map;
}
abstract public function processEditForm(
AphrontRequest $request,
array $values);
abstract public function extendEditForm(
AphrontRequest $request,
AphrontFormView $form,
array $values,
array $issues);
protected function renderConfigurationSecret($value) {
if (strlen($value)) {
return str_repeat('*', strlen($value));
}
return '';
}
public function isConfigurationSecret($value) {
return preg_match('/^\*+\z/', trim($value));
}
abstract public function canRunConfigurationTest();
public function runConfigurationTest() {
throw new PhutilMethodNotImplementedException();
}
/* -( Selecting Providers )------------------------------------------------ */
public static function getAllProviders() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->execute();
}
public function isEnabled() {
return $this->getProviderConfig()->getIsEnabled();
}
abstract public function isAcceptingLivePayments();
abstract public function getPaymentMethodDescription();
abstract public function getPaymentMethodIcon();
abstract public function getPaymentMethodProviderDescription();
final public function applyCharge(
PhortunePaymentMethod $payment_method,
PhortuneCharge $charge) {
$this->executeCharge($payment_method, $charge);
}
final public function refundCharge(
PhortuneCharge $charge,
PhortuneCharge $refund) {
$this->executeRefund($charge, $refund);
}
abstract protected function executeCharge(
PhortunePaymentMethod $payment_method,
PhortuneCharge $charge);
abstract protected function executeRefund(
PhortuneCharge $charge,
PhortuneCharge $refund);
abstract public function updateCharge(PhortuneCharge $charge);
/* -( Adding Payment Methods )--------------------------------------------- */
/**
* @task addmethod
*/
public function canCreatePaymentMethods() {
return false;
}
/**
* @task addmethod
*/
public function translateCreatePaymentMethodErrorCode($error_code) {
throw new PhutilMethodNotImplementedException();
}
/**
* @task addmethod
*/
public function getCreatePaymentMethodErrorMessage($error_code) {
throw new PhutilMethodNotImplementedException();
}
/**
* @task addmethod
*/
public function validateCreatePaymentMethodToken(array $token) {
throw new PhutilMethodNotImplementedException();
}
/**
* @task addmethod
*/
public function createPaymentMethodFromRequest(
AphrontRequest $request,
PhortunePaymentMethod $method,
array $token) {
throw new PhutilMethodNotImplementedException();
}
/**
* @task addmethod
*/
public function renderCreatePaymentMethodForm(
AphrontRequest $request,
array $errors) {
throw new PhutilMethodNotImplementedException();
}
public function getDefaultPaymentMethodDisplayName(
PhortunePaymentMethod $method) {
throw new PhutilMethodNotImplementedException();
}
/* -( One-Time Payments )-------------------------------------------------- */
public function canProcessOneTimePayments() {
return false;
}
public function renderOneTimePaymentButton(
PhortuneAccount $account,
PhortuneCart $cart,
PhabricatorUser $user) {
require_celerity_resource('phortune-css');
$description = $this->getPaymentMethodProviderDescription();
$details = $this->getPaymentMethodDescription();
$icon = id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
->setSpriteIcon($this->getPaymentMethodIcon());
$button = id(new PHUIButtonView())
->setSize(PHUIButtonView::BIG)
->setColor(PHUIButtonView::GREY)
->setIcon($icon)
->setText($description)
->setSubtext($details);
// NOTE: We generate a local URI to make sure the form picks up CSRF tokens.
$uri = $this->getControllerURI(
'checkout',
array(
'cartID' => $cart->getID(),
),
$local = true);
return phabricator_form(
$user,
array(
'action' => $uri,
'method' => 'POST',
),
$button);
}
/* -( Controllers )-------------------------------------------------------- */
final public function getControllerURI(
$action,
array $params = array(),
$local = false) {
$id = $this->getProviderConfig()->getID();
$app = PhabricatorApplication::getByClass('PhabricatorPhortuneApplication');
$path = $app->getBaseURI().'provider/'.$id.'/'.$action.'/';
- $uri = new PhutilURI($path);
- $uri->setQueryParams($params);
+ $uri = new PhutilURI($path, $params);
if ($local) {
return $uri;
} else {
return PhabricatorEnv::getURI((string)$uri);
}
}
public function canRespondToControllerAction($action) {
return false;
}
public function processControllerRequest(
PhortuneProviderActionController $controller,
AphrontRequest $request) {
throw new PhutilMethodNotImplementedException();
}
}
diff --git a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php
index bdaa4294b..046388101 100644
--- a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php
+++ b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php
@@ -1,386 +1,470 @@
<?php
final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
const STRIPE_PUBLISHABLE_KEY = 'stripe.publishable-key';
const STRIPE_SECRET_KEY = 'stripe.secret-key';
public function isAcceptingLivePayments() {
return preg_match('/_live_/', $this->getPublishableKey());
}
public function getName() {
return pht('Stripe');
}
public function getConfigureName() {
return pht('Add Stripe Payments Account');
}
public function getConfigureDescription() {
return pht(
'Allows you to accept credit or debit card payments with a '.
'stripe.com account.');
}
public function getConfigureProvidesDescription() {
return pht('This merchant accepts credit and debit cards via Stripe.');
}
public function getPaymentMethodDescription() {
return pht('Add Credit or Debit Card (US and Canada)');
}
public function getPaymentMethodIcon() {
return 'Stripe';
}
public function getPaymentMethodProviderDescription() {
return pht('Processed by Stripe');
}
public function getDefaultPaymentMethodDisplayName(
PhortunePaymentMethod $method) {
return pht('Credit/Debit Card');
}
public function getAllConfigurableProperties() {
return array(
self::STRIPE_PUBLISHABLE_KEY,
self::STRIPE_SECRET_KEY,
);
}
public function getAllConfigurableSecretProperties() {
return array(
self::STRIPE_SECRET_KEY,
);
}
public function processEditForm(
AphrontRequest $request,
array $values) {
$errors = array();
$issues = array();
if (!strlen($values[self::STRIPE_SECRET_KEY])) {
$errors[] = pht('Stripe Secret Key is required.');
$issues[self::STRIPE_SECRET_KEY] = pht('Required');
}
if (!strlen($values[self::STRIPE_PUBLISHABLE_KEY])) {
$errors[] = pht('Stripe Publishable Key is required.');
$issues[self::STRIPE_PUBLISHABLE_KEY] = pht('Required');
}
return array($errors, $issues, $values);
}
public function extendEditForm(
AphrontRequest $request,
AphrontFormView $form,
array $values,
array $issues) {
$form
->appendChild(
id(new AphrontFormTextControl())
->setName(self::STRIPE_SECRET_KEY)
->setValue($values[self::STRIPE_SECRET_KEY])
->setError(idx($issues, self::STRIPE_SECRET_KEY, true))
->setLabel(pht('Stripe Secret Key')))
->appendChild(
id(new AphrontFormTextControl())
->setName(self::STRIPE_PUBLISHABLE_KEY)
->setValue($values[self::STRIPE_PUBLISHABLE_KEY])
->setError(idx($issues, self::STRIPE_PUBLISHABLE_KEY, true))
->setLabel(pht('Stripe Publishable Key')));
}
public function getConfigureInstructions() {
return pht(
"To configure Stripe, register or log in to an existing account on ".
"[[https://stripe.com | stripe.com]]. Once logged in:\n\n".
" - Go to {nav icon=user, name=Your Account > Account Settings ".
"> API Keys}\n".
" - Copy the **Secret Key** and **Publishable Key** into the fields ".
"above.\n\n".
"You can either use the test keys to add this provider in test mode, ".
"or the live keys to accept live payments.");
}
public function canRunConfigurationTest() {
return true;
}
public function runConfigurationTest() {
$this->loadStripeAPILibraries();
$secret_key = $this->getSecretKey();
$account = Stripe_Account::retrieve($secret_key);
}
/**
* @phutil-external-symbol class Stripe_Charge
* @phutil-external-symbol class Stripe_CardError
* @phutil-external-symbol class Stripe_Account
*/
protected function executeCharge(
PhortunePaymentMethod $method,
PhortuneCharge $charge) {
$this->loadStripeAPILibraries();
$price = $charge->getAmountAsCurrency();
$secret_key = $this->getSecretKey();
$params = array(
'amount' => $price->getValueInUSDCents(),
'currency' => $price->getCurrency(),
'customer' => $method->getMetadataValue('stripe.customerID'),
'description' => $charge->getPHID(),
'capture' => true,
);
$stripe_charge = Stripe_Charge::create($params, $secret_key);
$id = $stripe_charge->id;
if (!$id) {
throw new Exception(pht('Stripe charge call did not return an ID!'));
}
$charge->setMetadataValue('stripe.chargeID', $id);
$charge->save();
}
protected function executeRefund(
PhortuneCharge $charge,
PhortuneCharge $refund) {
$this->loadStripeAPILibraries();
$charge_id = $charge->getMetadataValue('stripe.chargeID');
if (!$charge_id) {
throw new Exception(
pht('Unable to refund charge; no Stripe chargeID!'));
}
$refund_cents = $refund
->getAmountAsCurrency()
->negate()
->getValueInUSDCents();
$secret_key = $this->getSecretKey();
$params = array(
'amount' => $refund_cents,
);
$stripe_charge = Stripe_Charge::retrieve($charge_id, $secret_key);
$stripe_refund = $stripe_charge->refunds->create($params);
$id = $stripe_refund->id;
if (!$id) {
throw new Exception(pht('Stripe refund call did not return an ID!'));
}
$charge->setMetadataValue('stripe.refundID', $id);
$charge->save();
}
public function updateCharge(PhortuneCharge $charge) {
$this->loadStripeAPILibraries();
$charge_id = $charge->getMetadataValue('stripe.chargeID');
if (!$charge_id) {
throw new Exception(
pht('Unable to update charge; no Stripe chargeID!'));
}
$secret_key = $this->getSecretKey();
$stripe_charge = Stripe_Charge::retrieve($charge_id, $secret_key);
// TODO: Deal with disputes / chargebacks / surprising refunds.
}
private function getPublishableKey() {
return $this
->getProviderConfig()
->getMetadataValue(self::STRIPE_PUBLISHABLE_KEY);
}
private function getSecretKey() {
return $this
->getProviderConfig()
->getMetadataValue(self::STRIPE_SECRET_KEY);
}
/* -( Adding Payment Methods )--------------------------------------------- */
public function canCreatePaymentMethods() {
return true;
}
/**
* @phutil-external-symbol class Stripe_Token
* @phutil-external-symbol class Stripe_Customer
*/
public function createPaymentMethodFromRequest(
AphrontRequest $request,
PhortunePaymentMethod $method,
array $token) {
$this->loadStripeAPILibraries();
- $errors = array();
-
$secret_key = $this->getSecretKey();
$stripe_token = $token['stripeCardToken'];
// First, make sure the token is valid.
$info = id(new Stripe_Token())->retrieve($stripe_token, $secret_key);
$account_phid = $method->getAccountPHID();
$author_phid = $method->getAuthorPHID();
$params = array(
'card' => $stripe_token,
'description' => $account_phid.':'.$author_phid,
);
// Then, we need to create a Customer in order to be able to charge
// the card more than once. We create one Customer for each card;
// they do not map to PhortuneAccounts because we allow an account to
// have more than one active card.
- $customer = Stripe_Customer::create($params, $secret_key);
+ try {
+ $customer = Stripe_Customer::create($params, $secret_key);
+ } catch (Stripe_CardError $ex) {
+ $display_exception = $this->newDisplayExceptionFromCardError($ex);
+ if ($display_exception) {
+ throw $display_exception;
+ }
+ throw $ex;
+ }
$card = $info->card;
$method
->setBrand($card->brand)
->setLastFourDigits($card->last4)
->setExpires($card->exp_year, $card->exp_month)
->setMetadata(
array(
'type' => 'stripe.customer',
'stripe.customerID' => $customer->id,
'stripe.cardToken' => $stripe_token,
));
-
- return $errors;
}
public function renderCreatePaymentMethodForm(
AphrontRequest $request,
array $errors) {
$src = 'https://js.stripe.com/v2/';
$ccform = id(new PhortuneCreditCardForm())
->setSecurityAssurance(
pht('Payments are processed securely by Stripe.'))
->setUser($request->getUser())
->setErrors($errors)
->addScript($src);
CelerityAPI::getStaticResourceResponse()
->addContentSecurityPolicyURI('script-src', $src)
->addContentSecurityPolicyURI('frame-src', $src);
Javelin::initBehavior(
'stripe-payment-form',
array(
'stripePublishableKey' => $this->getPublishableKey(),
'formID' => $ccform->getFormID(),
));
return $ccform->buildForm();
}
private function getStripeShortErrorCode($error_code) {
$prefix = 'cc:stripe:';
if (strncmp($error_code, $prefix, strlen($prefix))) {
return null;
}
return substr($error_code, strlen($prefix));
}
public function validateCreatePaymentMethodToken(array $token) {
return isset($token['stripeCardToken']);
}
public function translateCreatePaymentMethodErrorCode($error_code) {
$short_code = $this->getStripeShortErrorCode($error_code);
if ($short_code) {
static $map = array(
'error:invalid_number' => PhortuneErrCode::ERR_CC_INVALID_NUMBER,
'error:invalid_cvc' => PhortuneErrCode::ERR_CC_INVALID_CVC,
'error:invalid_expiry_month' => PhortuneErrCode::ERR_CC_INVALID_EXPIRY,
'error:invalid_expiry_year' => PhortuneErrCode::ERR_CC_INVALID_EXPIRY,
);
if (isset($map[$short_code])) {
return $map[$short_code];
}
}
return $error_code;
}
/**
* See https://stripe.com/docs/api#errors for more information on possible
* errors.
*/
public function getCreatePaymentMethodErrorMessage($error_code) {
$short_code = $this->getStripeShortErrorCode($error_code);
if (!$short_code) {
return null;
}
switch ($short_code) {
case 'error:incorrect_number':
$error_key = 'number';
$message = pht('Invalid or incorrect credit card number.');
break;
case 'error:incorrect_cvc':
$error_key = 'cvc';
$message = pht('Card CVC is invalid or incorrect.');
break;
$error_key = 'exp';
$message = pht('Card expiration date is invalid or incorrect.');
break;
case 'error:invalid_expiry_month':
case 'error:invalid_expiry_year':
case 'error:invalid_cvc':
case 'error:invalid_number':
// NOTE: These should be translated into Phortune error codes earlier,
// so we don't expect to receive them here. They are listed for clarity
// and completeness. If we encounter one, we treat it as an unknown
// error.
break;
case 'error:invalid_amount':
case 'error:missing':
case 'error:card_declined':
case 'error:expired_card':
case 'error:duplicate_transaction':
case 'error:processing_error':
default:
// NOTE: These errors currently don't receive a detailed message.
// NOTE: We can also end up here with "http:nnn" messages.
// TODO: At least some of these should have a better message, or be
// translated into common errors above.
break;
}
return null;
}
private function loadStripeAPILibraries() {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/stripe-php/lib/Stripe.php';
}
+
+ private function newDisplayExceptionFromCardError(Stripe_CardError $ex) {
+ $body = $ex->getJSONBody();
+ if (!$body) {
+ return null;
+ }
+
+ $map = idx($body, 'error');
+ if (!$map) {
+ return null;
+ }
+
+ $view = array();
+
+ $message = idx($map, 'message');
+
+ $view[] = id(new PHUIInfoView())
+ ->setErrors(array($message));
+
+ $view[] = phutil_tag(
+ 'div',
+ array(
+ 'class' => 'mlt mlb',
+ ),
+ pht('Additional details about this error:'));
+
+ $rows = array();
+
+ $rows[] = array(
+ pht('Error Code'),
+ idx($map, 'code'),
+ );
+
+ $rows[] = array(
+ pht('Error Type'),
+ idx($map, 'type'),
+ );
+
+ $param = idx($map, 'param');
+ if (strlen($param)) {
+ $rows[] = array(
+ pht('Error Param'),
+ $param,
+ );
+ }
+
+ $decline_code = idx($map, 'decline_code');
+ if (strlen($decline_code)) {
+ $rows[] = array(
+ pht('Decline Code'),
+ $decline_code,
+ );
+ }
+
+ $doc_url = idx($map, 'doc_url');
+ if ($doc_url) {
+ $rows[] = array(
+ pht('Learn More'),
+ phutil_tag(
+ 'a',
+ array(
+ 'href' => $doc_url,
+ 'target' => '_blank',
+ ),
+ $doc_url),
+ );
+ }
+
+ $view[] = id(new AphrontTableView($rows))
+ ->setColumnClasses(
+ array(
+ 'header',
+ 'wide',
+ ));
+
+ return id(new PhortuneDisplayException(get_class($ex)))
+ ->setView($view);
+ }
+
+
}
diff --git a/src/applications/phortune/storage/PhortuneAccountTransaction.php b/src/applications/phortune/storage/PhortuneAccountTransaction.php
index 6733cbe87..e333ef4a2 100644
--- a/src/applications/phortune/storage/PhortuneAccountTransaction.php
+++ b/src/applications/phortune/storage/PhortuneAccountTransaction.php
@@ -1,22 +1,18 @@
<?php
final class PhortuneAccountTransaction
extends PhabricatorModularTransaction {
public function getApplicationName() {
return 'phortune';
}
public function getApplicationTransactionType() {
return PhortuneAccountPHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return null;
- }
-
public function getBaseTransactionClass() {
return 'PhortuneAccountTransactionType';
}
}
diff --git a/src/applications/phortune/storage/PhortuneCartTransaction.php b/src/applications/phortune/storage/PhortuneCartTransaction.php
index 41790011a..c7a1e36e7 100644
--- a/src/applications/phortune/storage/PhortuneCartTransaction.php
+++ b/src/applications/phortune/storage/PhortuneCartTransaction.php
@@ -1,91 +1,87 @@
<?php
final class PhortuneCartTransaction
extends PhabricatorApplicationTransaction {
const TYPE_CREATED = 'cart:created';
const TYPE_HOLD = 'cart:hold';
const TYPE_REVIEW = 'cart:review';
const TYPE_CANCEL = 'cart:cancel';
const TYPE_REFUND = 'cart:refund';
const TYPE_PURCHASED = 'cart:purchased';
const TYPE_INVOICED = 'cart:invoiced';
public function getApplicationName() {
return 'phortune';
}
public function getApplicationTransactionType() {
return PhortuneCartPHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return null;
- }
-
public function shouldHideForMail(array $xactions) {
switch ($this->getTransactionType()) {
case self::TYPE_CREATED:
return true;
}
return parent::shouldHideForMail($xactions);
}
public function getTitle() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_CREATED:
return pht('This order was created.');
case self::TYPE_HOLD:
return pht('This order was put on hold until payment clears.');
case self::TYPE_REVIEW:
return pht(
'This order was flagged for manual processing by the merchant.');
case self::TYPE_CANCEL:
return pht('This order was cancelled.');
case self::TYPE_REFUND:
return pht('This order was refunded.');
case self::TYPE_PURCHASED:
return pht('Payment for this order was completed.');
case self::TYPE_INVOICED:
return pht('This order was invoiced.');
}
return parent::getTitle();
}
public function getTitleForMail() {
switch ($this->getTransactionType()) {
case self::TYPE_INVOICED:
return pht('You have a new invoice due.');
}
return parent::getTitleForMail();
}
public function getActionName() {
switch ($this->getTransactionType()) {
case self::TYPE_CREATED:
return pht('Created');
case self::TYPE_HOLD:
return pht('Hold');
case self::TYPE_REVIEW:
return pht('Review');
case self::TYPE_CANCEL:
return pht('Cancelled');
case self::TYPE_REFUND:
return pht('Refunded');
case self::TYPE_PURCHASED:
return pht('Complete');
case self::TYPE_INVOICED:
return pht('New Invoice');
}
return parent::getActionName();
}
}
diff --git a/src/applications/phortune/storage/PhortuneMerchantTransaction.php b/src/applications/phortune/storage/PhortuneMerchantTransaction.php
index 3befb1221..976259c53 100644
--- a/src/applications/phortune/storage/PhortuneMerchantTransaction.php
+++ b/src/applications/phortune/storage/PhortuneMerchantTransaction.php
@@ -1,22 +1,18 @@
<?php
final class PhortuneMerchantTransaction
extends PhabricatorModularTransaction {
public function getApplicationName() {
return 'phortune';
}
public function getApplicationTransactionType() {
return PhortuneMerchantPHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return null;
- }
-
public function getBaseTransactionClass() {
return 'PhortuneMerchantTransactionType';
}
}
diff --git a/src/applications/phortune/storage/PhortunePaymentProviderConfigTransaction.php b/src/applications/phortune/storage/PhortunePaymentProviderConfigTransaction.php
index 08872d48f..9241c7ae0 100644
--- a/src/applications/phortune/storage/PhortunePaymentProviderConfigTransaction.php
+++ b/src/applications/phortune/storage/PhortunePaymentProviderConfigTransaction.php
@@ -1,57 +1,53 @@
<?php
final class PhortunePaymentProviderConfigTransaction
extends PhabricatorApplicationTransaction {
const TYPE_CREATE = 'paymentprovider:create';
const TYPE_PROPERTY = 'paymentprovider:property';
const TYPE_ENABLE = 'paymentprovider:enable';
const PROPERTY_KEY = 'provider-property';
public function getApplicationName() {
return 'phortune';
}
public function getApplicationTransactionType() {
return PhortunePaymentProviderPHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return null;
- }
-
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_CREATE:
return pht(
'%s created this payment provider.',
$this->renderHandleLink($author_phid));
case self::TYPE_ENABLE:
if ($new) {
return pht(
'%s enabled this payment provider.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s disabled this payment provider.',
$this->renderHandleLink($author_phid));
}
case self::TYPE_PROPERTY:
// TODO: Allow providers to improve this.
return pht(
'%s edited a property of this payment provider.',
$this->renderHandleLink($author_phid));
break;
}
return parent::getTitle();
}
}
diff --git a/src/applications/phrequent/query/PhrequentUserTimeQuery.php b/src/applications/phrequent/query/PhrequentUserTimeQuery.php
index cf5122c02..6400771a0 100644
--- a/src/applications/phrequent/query/PhrequentUserTimeQuery.php
+++ b/src/applications/phrequent/query/PhrequentUserTimeQuery.php
@@ -1,333 +1,332 @@
<?php
final class PhrequentUserTimeQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
const ORDER_ID_ASC = 0;
const ORDER_ID_DESC = 1;
const ORDER_STARTED_ASC = 2;
const ORDER_STARTED_DESC = 3;
const ORDER_ENDED_ASC = 4;
const ORDER_ENDED_DESC = 5;
const ENDED_YES = 0;
const ENDED_NO = 1;
const ENDED_ALL = 2;
private $ids;
private $userPHIDs;
private $objectPHIDs;
private $ended = self::ENDED_ALL;
private $needPreemptingEvents;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withUserPHIDs(array $user_phids) {
$this->userPHIDs = $user_phids;
return $this;
}
public function withObjectPHIDs(array $object_phids) {
$this->objectPHIDs = $object_phids;
return $this;
}
public function withEnded($ended) {
$this->ended = $ended;
return $this;
}
public function setOrder($order) {
switch ($order) {
case self::ORDER_ID_ASC:
$this->setOrderVector(array('-id'));
break;
case self::ORDER_ID_DESC:
$this->setOrderVector(array('id'));
break;
case self::ORDER_STARTED_ASC:
$this->setOrderVector(array('-start', '-id'));
break;
case self::ORDER_STARTED_DESC:
$this->setOrderVector(array('start', 'id'));
break;
case self::ORDER_ENDED_ASC:
$this->setOrderVector(array('-end', '-id'));
break;
case self::ORDER_ENDED_DESC:
$this->setOrderVector(array('end', 'id'));
break;
default:
throw new Exception(pht('Unknown order "%s".', $order));
}
return $this;
}
public function needPreemptingEvents($need_events) {
$this->needPreemptingEvents = $need_events;
return $this;
}
protected function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->userPHIDs !== null) {
$where[] = qsprintf(
$conn,
'userPHID IN (%Ls)',
$this->userPHIDs);
}
if ($this->objectPHIDs !== null) {
$where[] = qsprintf(
$conn,
'objectPHID IN (%Ls)',
$this->objectPHIDs);
}
switch ($this->ended) {
case self::ENDED_ALL:
break;
case self::ENDED_YES:
$where[] = qsprintf(
$conn,
'dateEnded IS NOT NULL');
break;
case self::ENDED_NO:
$where[] = qsprintf(
$conn,
'dateEnded IS NULL');
break;
default:
throw new Exception(pht("Unknown ended '%s'!", $this->ended));
}
$where[] = $this->buildPagingClause($conn);
return $this->formatWhereClause($conn, $where);
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'start' => array(
'column' => 'dateStarted',
'type' => 'int',
),
'end' => array(
'column' => 'dateEnded',
'type' => 'int',
'null' => 'head',
),
);
}
- protected function getPagingValueMap($cursor, array $keys) {
- $usertime = $this->loadCursorObject($cursor);
+ protected function newPagingMapFromPartialObject($object) {
return array(
- 'id' => $usertime->getID(),
- 'start' => $usertime->getDateStarted(),
- 'end' => $usertime->getDateEnded(),
+ 'id' => (int)$object->getID(),
+ 'start' => (int)$object->getDateStarted(),
+ 'end' => (int)$object->getDateEnded(),
);
}
protected function loadPage() {
$usertime = new PhrequentUserTime();
$conn = $usertime->establishConnection('r');
$data = queryfx_all(
$conn,
'SELECT usertime.* FROM %T usertime %Q %Q %Q',
$usertime->getTableName(),
$this->buildWhereClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
return $usertime->loadAllFromArray($data);
}
protected function didFilterPage(array $page) {
if ($this->needPreemptingEvents) {
$usertime = new PhrequentUserTime();
$conn_r = $usertime->establishConnection('r');
$preempt = array();
foreach ($page as $event) {
$preempt[] = qsprintf(
$conn_r,
'(userPHID = %s AND
(dateStarted BETWEEN %d AND %d) AND
(dateEnded IS NULL OR dateEnded > %d))',
$event->getUserPHID(),
$event->getDateStarted(),
nonempty($event->getDateEnded(), PhabricatorTime::getNow()),
$event->getDateStarted());
}
$preempting_events = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE %LO ORDER BY dateStarted ASC, id ASC',
$usertime->getTableName(),
$preempt);
$preempting_events = $usertime->loadAllFromArray($preempting_events);
$preempting_events = mgroup($preempting_events, 'getUserPHID');
foreach ($page as $event) {
$e_start = $event->getDateStarted();
$e_end = $event->getDateEnded();
$select = array();
$user_events = idx($preempting_events, $event->getUserPHID(), array());
foreach ($user_events as $u_event) {
if ($u_event->getID() == $event->getID()) {
// Don't allow an event to preempt itself.
continue;
}
$u_start = $u_event->getDateStarted();
$u_end = $u_event->getDateEnded();
if ($u_start < $e_start) {
// This event started before our event started, so it's not
// preempting us.
continue;
}
if ($u_start == $e_start) {
if ($u_event->getID() < $event->getID()) {
// This event started at the same time as our event started,
// but has a lower ID, so it's not preempting us.
continue;
}
}
if (($e_end !== null) && ($u_start > $e_end)) {
// Our event has ended, and this event started after it ended.
continue;
}
if (($u_end !== null) && ($u_end < $e_start)) {
// This event ended before our event began.
continue;
}
$select[] = $u_event;
}
$event->attachPreemptingEvents($select);
}
}
return $page;
}
/* -( Helper Functions ) --------------------------------------------------- */
public static function getEndedSearchOptions() {
return array(
self::ENDED_ALL => pht('All'),
self::ENDED_NO => pht('No'),
self::ENDED_YES => pht('Yes'),
);
}
public static function getOrderSearchOptions() {
return array(
self::ORDER_STARTED_ASC => pht('by furthest start date'),
self::ORDER_STARTED_DESC => pht('by nearest start date'),
self::ORDER_ENDED_ASC => pht('by furthest end date'),
self::ORDER_ENDED_DESC => pht('by nearest end date'),
);
}
public static function getUserTotalObjectsTracked(
PhabricatorUser $user,
$limit = PHP_INT_MAX) {
$usertime_dao = new PhrequentUserTime();
$conn = $usertime_dao->establishConnection('r');
$count = queryfx_one(
$conn,
'SELECT COUNT(usertime.id) N FROM %T usertime '.
'WHERE usertime.userPHID = %s '.
'AND usertime.dateEnded IS NULL '.
'LIMIT %d',
$usertime_dao->getTableName(),
$user->getPHID(),
$limit);
return $count['N'];
}
public static function isUserTrackingObject(
PhabricatorUser $user,
$phid) {
$usertime_dao = new PhrequentUserTime();
$conn = $usertime_dao->establishConnection('r');
$count = queryfx_one(
$conn,
'SELECT COUNT(usertime.id) N FROM %T usertime '.
'WHERE usertime.userPHID = %s '.
'AND usertime.objectPHID = %s '.
'AND usertime.dateEnded IS NULL',
$usertime_dao->getTableName(),
$user->getPHID(),
$phid);
return $count['N'] > 0;
}
public static function getUserTimeSpentOnObject(
PhabricatorUser $user,
$phid) {
$usertime_dao = new PhrequentUserTime();
$conn = $usertime_dao->establishConnection('r');
// First calculate all the time spent where the
// usertime blocks have ended.
$sum_ended = queryfx_one(
$conn,
'SELECT SUM(usertime.dateEnded - usertime.dateStarted) N '.
'FROM %T usertime '.
'WHERE usertime.userPHID = %s '.
'AND usertime.objectPHID = %s '.
'AND usertime.dateEnded IS NOT NULL',
$usertime_dao->getTableName(),
$user->getPHID(),
$phid);
// Now calculate the time spent where the usertime
// blocks have not yet ended.
$sum_not_ended = queryfx_one(
$conn,
'SELECT SUM(UNIX_TIMESTAMP() - usertime.dateStarted) N '.
'FROM %T usertime '.
'WHERE usertime.userPHID = %s '.
'AND usertime.objectPHID = %s '.
'AND usertime.dateEnded IS NULL',
$usertime_dao->getTableName(),
$user->getPHID(),
$phid);
return $sum_ended['N'] + $sum_not_ended['N'];
}
public function getQueryApplicationClass() {
return 'PhabricatorPhrequentApplication';
}
}
diff --git a/src/applications/phriction/editor/PhrictionTransactionEditor.php b/src/applications/phriction/editor/PhrictionTransactionEditor.php
index d09ff5d55..1476b24c4 100644
--- a/src/applications/phriction/editor/PhrictionTransactionEditor.php
+++ b/src/applications/phriction/editor/PhrictionTransactionEditor.php
@@ -1,574 +1,579 @@
<?php
final class PhrictionTransactionEditor
extends PhabricatorApplicationTransactionEditor {
const VALIDATE_CREATE_ANCESTRY = 'create';
const VALIDATE_MOVE_ANCESTRY = 'move';
private $description;
private $oldContent;
private $newContent;
private $moveAwayDocument;
private $skipAncestorCheck;
private $contentVersion;
private $processContentVersionError = true;
private $contentDiffURI;
public function setDescription($description) {
$this->description = $description;
return $this;
}
private function getDescription() {
return $this->description;
}
private function setOldContent(PhrictionContent $content) {
$this->oldContent = $content;
return $this;
}
public function getOldContent() {
return $this->oldContent;
}
private function setNewContent(PhrictionContent $content) {
$this->newContent = $content;
return $this;
}
public function getNewContent() {
return $this->newContent;
}
public function setSkipAncestorCheck($bool) {
$this->skipAncestorCheck = $bool;
return $this;
}
public function getSkipAncestorCheck() {
return $this->skipAncestorCheck;
}
public function setContentVersion($version) {
$this->contentVersion = $version;
return $this;
}
public function getContentVersion() {
return $this->contentVersion;
}
public function setProcessContentVersionError($process) {
$this->processContentVersionError = $process;
return $this;
}
public function getProcessContentVersionError() {
return $this->processContentVersionError;
}
public function setMoveAwayDocument(PhrictionDocument $document) {
$this->moveAwayDocument = $document;
return $this;
}
public function setShouldPublishContent(
PhrictionDocument $object,
$publish) {
if ($publish) {
$content_phid = $this->getNewContent()->getPHID();
} else {
$content_phid = $this->getOldContent()->getPHID();
}
$object->setContentPHID($content_phid);
return $this;
}
public function getEditorApplicationClass() {
return 'PhabricatorPhrictionApplication';
}
public function getEditorObjectsDescription() {
return pht('Phriction Documents');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$this->setOldContent($object->getContent());
return parent::expandTransactions($object, $xactions);
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$xactions = parent::expandTransaction($object, $xaction);
switch ($xaction->getTransactionType()) {
case PhrictionDocumentContentTransaction::TRANSACTIONTYPE:
if ($this->getIsNewObject()) {
break;
}
$content = $xaction->getNewValue();
if ($content === '') {
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(
PhrictionDocumentDeleteTransaction::TRANSACTIONTYPE)
->setNewValue(true)
->setMetadataValue('contentDelete', true);
}
break;
case PhrictionDocumentMoveToTransaction::TRANSACTIONTYPE:
$document = $xaction->getNewValue();
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($document->getViewPolicy());
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
->setNewValue($document->getEditPolicy());
break;
default:
break;
}
return $xactions;
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
if ($this->hasNewDocumentContent()) {
$content = $this->getNewDocumentContent($object);
$content
->setDocumentPHID($object->getPHID())
->save();
}
if ($this->getIsNewObject() && !$this->getSkipAncestorCheck()) {
// Stub out empty parent documents if they don't exist
$ancestral_slugs = PhabricatorSlug::getAncestry($object->getSlug());
if ($ancestral_slugs) {
$ancestors = id(new PhrictionDocumentQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withSlugs($ancestral_slugs)
->needContent(true)
->execute();
$ancestors = mpull($ancestors, null, 'getSlug');
$stub_type = PhrictionChangeType::CHANGE_STUB;
foreach ($ancestral_slugs as $slug) {
$ancestor_doc = idx($ancestors, $slug);
// We check for change type to prevent near-infinite recursion
if (!$ancestor_doc && $content->getChangeType() != $stub_type) {
$ancestor_doc = PhrictionDocument::initializeNewDocument(
$this->getActor(),
$slug);
$stub_xactions = array();
$stub_xactions[] = id(new PhrictionTransaction())
->setTransactionType(
PhrictionDocumentTitleTransaction::TRANSACTIONTYPE)
->setNewValue(PhabricatorSlug::getDefaultTitle($slug))
->setMetadataValue('stub:create:phid', $object->getPHID());
$stub_xactions[] = id(new PhrictionTransaction())
->setTransactionType(
PhrictionDocumentContentTransaction::TRANSACTIONTYPE)
->setNewValue('')
->setMetadataValue('stub:create:phid', $object->getPHID());
$stub_xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($object->getViewPolicy());
$stub_xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
->setNewValue($object->getEditPolicy());
$sub_editor = id(new PhrictionTransactionEditor())
->setActor($this->getActor())
->setContentSource($this->getContentSource())
->setContinueOnNoEffect($this->getContinueOnNoEffect())
->setSkipAncestorCheck(true)
->setDescription(pht('Empty Parent Document'))
->applyTransactions($ancestor_doc, $stub_xactions);
}
}
}
}
if ($this->moveAwayDocument !== null) {
$move_away_xactions = array();
$move_away_xactions[] = id(new PhrictionTransaction())
->setTransactionType(
PhrictionDocumentMoveAwayTransaction::TRANSACTIONTYPE)
->setNewValue($object);
$sub_editor = id(new PhrictionTransactionEditor())
->setActor($this->getActor())
->setContentSource($this->getContentSource())
->setContinueOnNoEffect($this->getContinueOnNoEffect())
->setDescription($this->getDescription())
->applyTransactions($this->moveAwayDocument, $move_away_xactions);
}
// Compute the content diff URI for the publishing phase.
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhrictionDocumentContentTransaction::TRANSACTIONTYPE:
- $uri = id(new PhutilURI('/phriction/diff/'.$object->getID().'/'))
- ->alter('l', $this->getOldContent()->getVersion())
- ->alter('r', $this->getNewContent()->getVersion());
+ $params = array(
+ 'l' => $this->getOldContent()->getVersion(),
+ 'r' => $this->getNewContent()->getVersion(),
+ );
+
+ $path = '/phriction/diff/'.$object->getID().'/';
+ $uri = new PhutilURI($path, $params);
+
$this->contentDiffURI = (string)$uri;
break 2;
default:
break;
}
}
return $xactions;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailSubjectPrefix() {
return '[Phriction]';
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$this->getActingAsPHID(),
);
}
public function getMailTagsMap() {
return array(
PhrictionTransaction::MAILTAG_TITLE =>
pht("A document's title changes."),
PhrictionTransaction::MAILTAG_CONTENT =>
pht("A document's content changes."),
PhrictionTransaction::MAILTAG_DELETE =>
pht('A document is deleted.'),
PhrictionTransaction::MAILTAG_SUBSCRIBERS =>
pht('A document\'s subscribers change.'),
PhrictionTransaction::MAILTAG_OTHER =>
pht('Other document activity not listed above occurs.'),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PhrictionReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$title = $object->getContent()->getTitle();
return id(new PhabricatorMetaMTAMail())
->setSubject($title);
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
if ($this->getIsNewObject()) {
$body->addRemarkupSection(
pht('DOCUMENT CONTENT'),
$object->getContent()->getContent());
} else if ($this->contentDiffURI) {
$body->addLinkSection(
pht('DOCUMENT DIFF'),
PhabricatorEnv::getProductionURI($this->contentDiffURI));
}
$description = $object->getContent()->getDescription();
if (strlen($description)) {
$body->addTextSection(
pht('EDIT NOTES'),
$description);
}
$body->addLinkSection(
pht('DOCUMENT DETAIL'),
PhabricatorEnv::getProductionURI(
PhrictionDocument::getSlugURI($object->getSlug())));
return $body;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return $this->shouldSendMail($object, $xactions);
}
protected function getFeedRelatedPHIDs(
PhabricatorLiskDAO $object,
array $xactions) {
$phids = parent::getFeedRelatedPHIDs($object, $xactions);
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhrictionDocumentMoveToTransaction::TRANSACTIONTYPE:
$dict = $xaction->getNewValue();
$phids[] = $dict['phid'];
break;
}
}
return $phids;
}
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = parent::validateTransaction($object, $type, $xactions);
foreach ($xactions as $xaction) {
switch ($type) {
case PhrictionDocumentContentTransaction::TRANSACTIONTYPE:
if ($xaction->getMetadataValue('stub:create:phid')) {
break;
}
if ($this->getProcessContentVersionError()) {
$error = $this->validateContentVersion($object, $type, $xaction);
if ($error) {
$this->setProcessContentVersionError(false);
$errors[] = $error;
}
}
if ($this->getIsNewObject()) {
$ancestry_errors = $this->validateAncestry(
$object,
$type,
$xaction,
self::VALIDATE_CREATE_ANCESTRY);
if ($ancestry_errors) {
$errors = array_merge($errors, $ancestry_errors);
}
}
break;
case PhrictionDocumentMoveToTransaction::TRANSACTIONTYPE:
$source_document = $xaction->getNewValue();
$ancestry_errors = $this->validateAncestry(
$object,
$type,
$xaction,
self::VALIDATE_MOVE_ANCESTRY);
if ($ancestry_errors) {
$errors = array_merge($errors, $ancestry_errors);
}
$target_document = id(new PhrictionDocumentQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withSlugs(array($object->getSlug()))
->needContent(true)
->executeOne();
// Prevent overwrites and no-op moves.
$exists = PhrictionDocumentStatus::STATUS_EXISTS;
if ($target_document) {
$message = null;
if ($target_document->getSlug() == $source_document->getSlug()) {
$message = pht(
'You can not move a document to its existing location. '.
'Choose a different location to move the document to.');
} else if ($target_document->getStatus() == $exists) {
$message = pht(
'You can not move this document there, because it would '.
'overwrite an existing document which is already at that '.
'location. Move or delete the existing document first.');
}
if ($message !== null) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
$message,
$xaction);
$errors[] = $error;
}
}
break;
}
}
return $errors;
}
public function validateAncestry(
PhabricatorLiskDAO $object,
$type,
PhabricatorApplicationTransaction $xaction,
$verb) {
$errors = array();
// NOTE: We use the omnipotent user for these checks because policy
// doesn't matter; existence does.
$other_doc_viewer = PhabricatorUser::getOmnipotentUser();
$ancestral_slugs = PhabricatorSlug::getAncestry($object->getSlug());
if ($ancestral_slugs) {
$ancestors = id(new PhrictionDocumentQuery())
->setViewer($other_doc_viewer)
->withSlugs($ancestral_slugs)
->execute();
$ancestors = mpull($ancestors, null, 'getSlug');
foreach ($ancestral_slugs as $slug) {
$ancestor_doc = idx($ancestors, $slug);
if (!$ancestor_doc) {
$create_uri = '/phriction/edit/?slug='.$slug;
$create_link = phutil_tag(
'a',
array(
'href' => $create_uri,
),
$slug);
switch ($verb) {
case self::VALIDATE_MOVE_ANCESTRY:
$message = pht(
'Can not move document because the parent document with '.
'slug %s does not exist!',
$create_link);
break;
case self::VALIDATE_CREATE_ANCESTRY:
$message = pht(
'Can not create document because the parent document with '.
'slug %s does not exist!',
$create_link);
break;
}
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Missing Ancestor'),
$message,
$xaction);
$errors[] = $error;
}
}
}
return $errors;
}
private function validateContentVersion(
PhabricatorLiskDAO $object,
$type,
PhabricatorApplicationTransaction $xaction) {
$error = null;
if ($this->getContentVersion() &&
($object->getMaxVersion() != $this->getContentVersion())) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Edit Conflict'),
pht(
'Another user made changes to this document after you began '.
'editing it. Do you want to overwrite their changes? '.
'(If you choose to overwrite their changes, you should review '.
'the document edit history to see what you overwrote, and '.
'then make another edit to merge the changes if necessary.)'),
$xaction);
}
return $error;
}
protected function supportsSearch() {
return true;
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new PhrictionDocumentHeraldAdapter())
->setDocument($object);
}
private function hasNewDocumentContent() {
return (bool)$this->newContent;
}
public function getNewDocumentContent(PhrictionDocument $document) {
if (!$this->hasNewDocumentContent()) {
$content = $this->newDocumentContent($document);
// Generate a PHID now so we can populate "contentPHID" before saving
// the document to the database: the column is not nullable so we need
// a value.
$content_phid = $content->generatePHID();
$content->setPHID($content_phid);
$document->setContentPHID($content_phid);
$document->attachContent($content);
$document->setEditedEpoch(PhabricatorTime::getNow());
$document->setMaxVersion($content->getVersion());
$this->newContent = $content;
}
return $this->newContent;
}
private function newDocumentContent(PhrictionDocument $document) {
$content = id(new PhrictionContent())
->setSlug($document->getSlug())
->setAuthorPHID($this->getActingAsPHID())
->setChangeType(PhrictionChangeType::CHANGE_EDIT)
->setTitle($this->getOldContent()->getTitle())
->setContent($this->getOldContent()->getContent())
->setDescription('');
if (strlen($this->getDescription())) {
$content->setDescription($this->getDescription());
}
$content->setVersion($document->getMaxVersion() + 1);
return $content;
}
protected function getCustomWorkerState() {
return array(
'contentDiffURI' => $this->contentDiffURI,
);
}
protected function loadCustomWorkerState(array $state) {
$this->contentDiffURI = idx($state, 'contentDiffURI');
return $this;
}
}
diff --git a/src/applications/phriction/query/PhrictionDocumentQuery.php b/src/applications/phriction/query/PhrictionDocumentQuery.php
index c0737e59b..f56bbc28b 100644
--- a/src/applications/phriction/query/PhrictionDocumentQuery.php
+++ b/src/applications/phriction/query/PhrictionDocumentQuery.php
@@ -1,411 +1,419 @@
<?php
final class PhrictionDocumentQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $slugs;
private $depths;
private $slugPrefix;
private $slugsPrefix; // c4science custo
private $statuses;
private $parentPaths;
private $ancestorPaths;
private $needContent;
const ORDER_HIERARCHY = 'hierarchy';
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withSlugs(array $slugs) {
$this->slugs = $slugs;
return $this;
}
public function withDepths(array $depths) {
$this->depths = $depths;
return $this;
}
public function withSlugPrefix($slug_prefix) {
$this->slugPrefix = $slug_prefix;
return $this;
}
// c4science customization
public function withSlugsPrefix($slugs_prefix) {
$this->slugsPrefix = $slugs_prefix;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withParentPaths(array $paths) {
$this->parentPaths = $paths;
return $this;
}
public function withAncestorPaths(array $paths) {
$this->ancestorPaths = $paths;
return $this;
}
public function needContent($need_content) {
$this->needContent = $need_content;
return $this;
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
public function newResultObject() {
return new PhrictionDocument();
}
protected function willFilterPage(array $documents) {
if ($documents) {
$ancestor_slugs = array();
foreach ($documents as $key => $document) {
$document_slug = $document->getSlug();
foreach (PhabricatorSlug::getAncestry($document_slug) as $ancestor) {
$ancestor_slugs[$ancestor][] = $key;
}
}
if ($ancestor_slugs) {
$table = new PhrictionDocument();
$conn_r = $table->establishConnection('r');
$ancestors = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE slug IN (%Ls)',
$document->getTableName(),
array_keys($ancestor_slugs));
$ancestors = $table->loadAllFromArray($ancestors);
$ancestors = mpull($ancestors, null, 'getSlug');
foreach ($ancestor_slugs as $ancestor_slug => $document_keys) {
$ancestor = idx($ancestors, $ancestor_slug);
foreach ($document_keys as $document_key) {
$documents[$document_key]->attachAncestor(
$ancestor_slug,
$ancestor);
}
}
}
}
// To view a Phriction document, you must also be able to view all of the
// ancestor documents. Filter out documents which have ancestors that are
// not visible.
$document_map = array();
foreach ($documents as $document) {
$document_map[$document->getSlug()] = $document;
foreach ($document->getAncestors() as $key => $ancestor) {
if ($ancestor) {
$document_map[$key] = $ancestor;
}
}
}
$filtered_map = $this->applyPolicyFilter(
$document_map,
array(PhabricatorPolicyCapability::CAN_VIEW));
// Filter all of the documents where a parent is not visible.
foreach ($documents as $document_key => $document) {
// If the document itself is not visible, filter it.
if (!isset($filtered_map[$document->getSlug()])) {
$this->didRejectResult($documents[$document_key]);
unset($documents[$document_key]);
continue;
}
// If an ancestor exists but is not visible, filter the document.
foreach ($document->getAncestors() as $ancestor_key => $ancestor) {
if (!$ancestor) {
continue;
}
if (!isset($filtered_map[$ancestor_key])) {
$this->didRejectResult($documents[$document_key]);
unset($documents[$document_key]);
break;
}
}
}
if (!$documents) {
return $documents;
}
if ($this->needContent) {
$contents = id(new PhrictionContentQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs(mpull($documents, 'getContentPHID'))
->execute();
$contents = mpull($contents, null, 'getPHID');
foreach ($documents as $key => $document) {
$content_phid = $document->getContentPHID();
if (empty($contents[$content_phid])) {
unset($documents[$key]);
continue;
}
$document->attachContent($contents[$content_phid]);
}
}
return $documents;
}
+ protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
+ $select = parent::buildSelectClauseParts($conn);
+
+ if ($this->shouldJoinContentTable()) {
+ $select[] = qsprintf($conn, 'c.title');
+ }
+
+ return $select;
+ }
+
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
- if ($this->getOrderVector()->containsKey('updated')) {
+ if ($this->shouldJoinContentTable()) {
$content_dao = new PhrictionContent();
$joins[] = qsprintf(
$conn,
'JOIN %T c ON d.contentPHID = c.phid',
$content_dao->getTableName());
}
return $joins;
}
+ private function shouldJoinContentTable() {
+ if ($this->getOrderVector()->containsKey('title')) {
+ return true;
+ }
+
+ return false;
+ }
+
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'd.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'd.phid IN (%Ls)',
$this->phids);
}
if ($this->slugs !== null) {
$where[] = qsprintf(
$conn,
'd.slug IN (%Ls)',
$this->slugs);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'd.status IN (%Ls)',
$this->statuses);
}
if ($this->slugPrefix !== null) {
$where[] = qsprintf(
$conn,
'd.slug LIKE %>',
$this->slugPrefix);
}
// c4science customization
if ($this->slugsPrefix !== null) {
foreach($this->slugsPrefix as $prefix) {
$where[] = qsprintf(
$conn,
'd.slug LIKE %>',
$prefix);
}
}
if ($this->depths !== null) {
$where[] = qsprintf(
$conn,
'd.depth IN (%Ld)',
$this->depths);
}
if ($this->parentPaths !== null || $this->ancestorPaths !== null) {
$sets = array(
array(
'paths' => $this->parentPaths,
'parents' => true,
),
array(
'paths' => $this->ancestorPaths,
'parents' => false,
),
);
$paths = array();
foreach ($sets as $set) {
$set_paths = $set['paths'];
if ($set_paths === null) {
continue;
}
if (!$set_paths) {
throw new PhabricatorEmptyQueryException(
pht('No parent/ancestor paths specified.'));
}
$is_parents = $set['parents'];
foreach ($set_paths as $path) {
$path_normal = PhabricatorSlug::normalize($path);
if ($path !== $path_normal) {
throw new Exception(
pht(
'Document path "%s" is not a valid path. The normalized '.
'form of this path is "%s".',
$path,
$path_normal));
}
$depth = PhabricatorSlug::getDepth($path_normal);
if ($is_parents) {
$min_depth = $depth + 1;
$max_depth = $depth + 1;
} else {
$min_depth = $depth + 1;
$max_depth = null;
}
$paths[] = array(
$path_normal,
$min_depth,
$max_depth,
);
}
}
$path_clauses = array();
foreach ($paths as $path) {
$parts = array();
list($prefix, $min, $max) = $path;
// If we're getting children or ancestors of the root document, they
// aren't actually stored with the leading "/" in the database, so
// just skip this part of the clause.
if ($prefix !== '/') {
$parts[] = qsprintf(
$conn,
'd.slug LIKE %>',
$prefix);
}
if ($min !== null) {
$parts[] = qsprintf(
$conn,
'd.depth >= %d',
$min);
}
if ($max !== null) {
$parts[] = qsprintf(
$conn,
'd.depth <= %d',
$max);
}
if ($parts) {
$path_clauses[] = qsprintf($conn, '%LA', $parts);
}
}
if ($path_clauses) {
$where[] = qsprintf($conn, '%LO', $path_clauses);
}
}
return $where;
}
public function getBuiltinOrders() {
return parent::getBuiltinOrders() + array(
self::ORDER_HIERARCHY => array(
'vector' => array('depth', 'title', 'updated', 'id'),
'name' => pht('Hierarchy'),
),
);
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'depth' => array(
'table' => 'd',
'column' => 'depth',
'reverse' => true,
'type' => 'int',
),
'title' => array(
'table' => 'c',
'column' => 'title',
'reverse' => true,
'type' => 'string',
),
'updated' => array(
'table' => 'd',
'column' => 'editedEpoch',
'type' => 'int',
'unique' => false,
),
);
}
- protected function getPagingValueMap($cursor, array $keys) {
- $document = $this->loadCursorObject($cursor);
+ protected function newPagingMapFromCursorObject(
+ PhabricatorQueryCursor $cursor,
+ array $keys) {
+
+ $document = $cursor->getObject();
$map = array(
- 'id' => $document->getID(),
+ 'id' => (int)$document->getID(),
'depth' => $document->getDepth(),
- 'updated' => $document->getEditedEpoch(),
+ 'updated' => (int)$document->getEditedEpoch(),
);
- foreach ($keys as $key) {
- switch ($key) {
- case 'title':
- $map[$key] = $document->getContent()->getTitle();
- break;
- }
+ if (isset($keys['title'])) {
+ $map['title'] = $cursor->getRawRowProperty('title');
}
return $map;
}
- protected function willExecuteCursorQuery(
- PhabricatorCursorPagedPolicyAwareQuery $query) {
- $vector = $this->getOrderVector();
-
- if ($vector->containsKey('title')) {
- $query->needContent(true);
- }
- }
-
protected function getPrimaryTableAlias() {
return 'd';
}
public function getQueryApplicationClass() {
return 'PhabricatorPhrictionApplication';
}
}
diff --git a/src/applications/phurl/query/PhabricatorPhurlURLQuery.php b/src/applications/phurl/query/PhabricatorPhurlURLQuery.php
index 74cced077..6efbbd5b4 100644
--- a/src/applications/phurl/query/PhabricatorPhurlURLQuery.php
+++ b/src/applications/phurl/query/PhabricatorPhurlURLQuery.php
@@ -1,119 +1,112 @@
<?php
final class PhabricatorPhurlURLQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $names;
private $longURLs;
private $aliases;
private $authorPHIDs;
public function newResultObject() {
return new PhabricatorPhurlURL();
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withNameNgrams($ngrams) {
return $this->withNgramsConstraint(
id(new PhabricatorPhurlURLNameNgrams()),
$ngrams);
}
public function withLongURLs(array $long_urls) {
$this->longURLs = $long_urls;
return $this;
}
public function withAliases(array $aliases) {
$this->aliases = $aliases;
return $this;
}
public function withAuthorPHIDs(array $author_phids) {
$this->authorPHIDs = $author_phids;
return $this;
}
- protected function getPagingValueMap($cursor, array $keys) {
- $url = $this->loadCursorObject($cursor);
- return array(
- 'id' => $url->getID(),
- );
- }
-
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'url.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'url.phid IN (%Ls)',
$this->phids);
}
if ($this->authorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'url.authorPHID IN (%Ls)',
$this->authorPHIDs);
}
if ($this->names !== null) {
$where[] = qsprintf(
$conn,
'url.name IN (%Ls)',
$this->names);
}
if ($this->longURLs !== null) {
$where[] = qsprintf(
$conn,
'url.longURL IN (%Ls)',
$this->longURLs);
}
if ($this->aliases !== null) {
$where[] = qsprintf(
$conn,
'url.alias IN (%Ls)',
$this->aliases);
}
return $where;
}
protected function getPrimaryTableAlias() {
return 'url';
}
public function getQueryApplicationClass() {
return 'PhabricatorPhurlApplication';
}
}
diff --git a/src/applications/policy/codex/PhabricatorPolicyCodex.php b/src/applications/policy/codex/PhabricatorPolicyCodex.php
index 060a798e0..48e6d2f55 100644
--- a/src/applications/policy/codex/PhabricatorPolicyCodex.php
+++ b/src/applications/policy/codex/PhabricatorPolicyCodex.php
@@ -1,127 +1,131 @@
<?php
/**
* Rendering extensions that allows an object to render custom strings,
* descriptions and explanations for the policy system to help users
* understand complex policies.
*/
abstract class PhabricatorPolicyCodex
extends Phobject {
private $viewer;
private $object;
private $policy;
private $capability;
public function getPolicyShortName() {
return null;
}
public function getPolicyIcon() {
return null;
}
public function getPolicyTagClasses() {
return array();
}
public function getPolicySpecialRuleDescriptions() {
return array();
}
+ public function getPolicyForEdit($capability) {
+ return $this->getObject()->getPolicy($capability);
+ }
+
public function getDefaultPolicy() {
return PhabricatorPolicyQuery::getDefaultPolicyForObject(
$this->viewer,
$this->object,
$this->capability);
}
public function compareToDefaultPolicy(PhabricatorPolicy $policy) {
return null;
}
final public function getPolicySpecialRuleForCapability($capability) {
foreach ($this->getPolicySpecialRuleDescriptions() as $rule) {
if (in_array($capability, $rule->getCapabilities())) {
return $rule;
}
}
return null;
}
final protected function newRule() {
return new PhabricatorPolicyCodexRuleDescription();
}
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
final public function getViewer() {
return $this->viewer;
}
final public function setObject(PhabricatorPolicyCodexInterface $object) {
$this->object = $object;
return $this;
}
final public function getObject() {
return $this->object;
}
final public function setCapability($capability) {
$this->capability = $capability;
return $this;
}
final public function getCapability() {
return $this->capability;
}
final public function setPolicy(PhabricatorPolicy $policy) {
$this->policy = $policy;
return $this;
}
final public function getPolicy() {
return $this->policy;
}
final public static function newFromObject(
PhabricatorPolicyCodexInterface $object,
PhabricatorUser $viewer) {
if (!($object instanceof PhabricatorPolicyInterface)) {
throw new Exception(
pht(
'Object (of class "%s") implements interface "%s", but must also '.
'implement interface "%s".',
get_class($object),
'PhabricatorPolicyCodexInterface',
'PhabricatorPolicyInterface'));
}
$codex = $object->newPolicyCodex();
if (!($codex instanceof PhabricatorPolicyCodex)) {
throw new Exception(
pht(
'Object (of class "%s") implements interface "%s", but defines '.
'method "%s" incorrectly: this method must return an object of '.
'class "%s".',
get_class($object),
'PhabricatorPolicyCodexInterface',
'newPolicyCodex()',
__CLASS__));
}
$codex
->setObject($object)
->setViewer($viewer);
return $codex;
}
}
diff --git a/src/applications/policy/editor/PhabricatorPolicyEditEngineExtension.php b/src/applications/policy/editor/PhabricatorPolicyEditEngineExtension.php
index 568b7bc39..14a4768f2 100644
--- a/src/applications/policy/editor/PhabricatorPolicyEditEngineExtension.php
+++ b/src/applications/policy/editor/PhabricatorPolicyEditEngineExtension.php
@@ -1,133 +1,153 @@
<?php
final class PhabricatorPolicyEditEngineExtension
extends PhabricatorEditEngineExtension {
const EXTENSIONKEY = 'policy.policy';
public function getExtensionPriority() {
return 250;
}
public function isExtensionEnabled() {
return true;
}
public function getExtensionName() {
return pht('Policies');
}
public function supportsObject(
PhabricatorEditEngine $engine,
PhabricatorApplicationTransactionInterface $object) {
return ($object instanceof PhabricatorPolicyInterface);
}
public function buildCustomEditFields(
PhabricatorEditEngine $engine,
PhabricatorApplicationTransactionInterface $object) {
$viewer = $engine->getViewer();
$editor = $object->getApplicationTransactionEditor();
$types = $editor->getTransactionTypesForObject($object);
$types = array_fuse($types);
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->setObject($object)
->execute();
$map = array(
PhabricatorTransactions::TYPE_VIEW_POLICY => array(
'key' => 'policy.view',
'aliases' => array('view'),
'capability' => PhabricatorPolicyCapability::CAN_VIEW,
'label' => pht('View Policy'),
'description' => pht('Controls who can view the object.'),
'description.conduit' => pht('Change the view policy of the object.'),
'edit' => 'view',
),
PhabricatorTransactions::TYPE_EDIT_POLICY => array(
'key' => 'policy.edit',
'aliases' => array('edit'),
'capability' => PhabricatorPolicyCapability::CAN_EDIT,
'label' => pht('Edit Policy'),
'description' => pht('Controls who can edit the object.'),
'description.conduit' => pht('Change the edit policy of the object.'),
'edit' => 'edit',
),
PhabricatorTransactions::TYPE_JOIN_POLICY => array(
'key' => 'policy.join',
'aliases' => array('join'),
'capability' => PhabricatorPolicyCapability::CAN_JOIN,
'label' => pht('Join Policy'),
'description' => pht('Controls who can join the object.'),
'description.conduit' => pht('Change the join policy of the object.'),
'edit' => 'join',
),
);
+ if ($object instanceof PhabricatorPolicyCodexInterface) {
+ $codex = PhabricatorPolicyCodex::newFromObject(
+ $object,
+ $viewer);
+ } else {
+ $codex = null;
+ }
+
$fields = array();
foreach ($map as $type => $spec) {
if (empty($types[$type])) {
continue;
}
$capability = $spec['capability'];
$key = $spec['key'];
$aliases = $spec['aliases'];
$label = $spec['label'];
$description = $spec['description'];
$conduit_description = $spec['description.conduit'];
$edit = $spec['edit'];
+ // Objects may present a policy value to the edit workflow that is
+ // different from their nominal policy value: for example, when tasks
+ // are locked, they appear as "Editable By: No One" to other applications
+ // but we still want to edit the actual policy stored in the database
+ // when we show the user a form with a policy control in it.
+
+ if ($codex) {
+ $policy_value = $codex->getPolicyForEdit($capability);
+ } else {
+ $policy_value = $object->getPolicy($capability);
+ }
+
$policy_field = id(new PhabricatorPolicyEditField())
->setKey($key)
->setLabel($label)
->setAliases($aliases)
->setIsCopyable(true)
->setCapability($capability)
->setPolicies($policies)
->setTransactionType($type)
->setEditTypeKey($edit)
->setDescription($description)
->setConduitDescription($conduit_description)
->setConduitTypeDescription(pht('New policy PHID or constant.'))
- ->setValue($object->getPolicy($capability));
+ ->setValue($policy_value);
$fields[] = $policy_field;
if ($object instanceof PhabricatorSpacesInterface) {
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
$type_space = PhabricatorTransactions::TYPE_SPACE;
if (isset($types[$type_space])) {
$space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID(
$object);
$space_field = id(new PhabricatorSpaceEditField())
->setKey('spacePHID')
->setLabel(pht('Space'))
->setEditTypeKey('space')
->setIsCopyable(true)
->setIsLockable(false)
->setIsReorderable(false)
->setAliases(array('space', 'policy.space'))
->setTransactionType($type_space)
->setDescription(pht('Select a space for the object.'))
->setConduitDescription(
pht('Shift the object between spaces.'))
->setConduitTypeDescription(pht('New space PHID.'))
->setValue($space_phid);
$fields[] = $space_field;
$space_field->setPolicyField($policy_field);
$policy_field->setSpaceField($space_field);
}
}
}
}
return $fields;
}
}
diff --git a/src/applications/policy/filter/PhabricatorPolicyFilter.php b/src/applications/policy/filter/PhabricatorPolicyFilter.php
index fb03936ec..a5c9f356f 100644
--- a/src/applications/policy/filter/PhabricatorPolicyFilter.php
+++ b/src/applications/policy/filter/PhabricatorPolicyFilter.php
@@ -1,973 +1,974 @@
<?php
final class PhabricatorPolicyFilter extends Phobject {
private $viewer;
private $objects;
private $capabilities;
private $raisePolicyExceptions;
private $userProjects;
private $customPolicies = array();
private $objectPolicies = array();
private $forcedPolicy;
public static function mustRetainCapability(
PhabricatorUser $user,
PhabricatorPolicyInterface $object,
$capability) {
if (!self::hasCapability($user, $object, $capability)) {
throw new Exception(
pht(
"You can not make that edit, because it would remove your ability ".
"to '%s' the object.",
$capability));
}
}
public static function requireCapability(
PhabricatorUser $user,
PhabricatorPolicyInterface $object,
$capability) {
$filter = id(new PhabricatorPolicyFilter())
->setViewer($user)
->requireCapabilities(array($capability))
->raisePolicyExceptions(true)
->apply(array($object));
}
/**
* Perform a capability check, acting as though an object had a specific
* policy. This is primarily used to check if a policy is valid (for example,
* to prevent users from editing away their ability to edit an object).
*
* Specifically, a check like this:
*
* PhabricatorPolicyFilter::requireCapabilityWithForcedPolicy(
* $viewer,
* $object,
* PhabricatorPolicyCapability::CAN_EDIT,
* $potential_new_policy);
*
* ...will throw a @{class:PhabricatorPolicyException} if the new policy would
* remove the user's ability to edit the object.
*
* @param PhabricatorUser The viewer to perform a policy check for.
* @param PhabricatorPolicyInterface The object to perform a policy check on.
* @param string Capability to test.
* @param string Perform the test as though the object has this
* policy instead of the policy it actually has.
* @return void
*/
public static function requireCapabilityWithForcedPolicy(
PhabricatorUser $viewer,
PhabricatorPolicyInterface $object,
$capability,
$forced_policy) {
id(new PhabricatorPolicyFilter())
->setViewer($viewer)
->requireCapabilities(array($capability))
->raisePolicyExceptions(true)
->forcePolicy($forced_policy)
->apply(array($object));
}
public static function hasCapability(
PhabricatorUser $user,
PhabricatorPolicyInterface $object,
$capability) {
$filter = new PhabricatorPolicyFilter();
$filter->setViewer($user);
$filter->requireCapabilities(array($capability));
$result = $filter->apply(array($object));
return (count($result) == 1);
}
public static function canInteract(
PhabricatorUser $user,
PhabricatorPolicyInterface $object) {
$capabilities = $object->getCapabilities();
$capabilities = array_fuse($capabilities);
$can_interact = PhabricatorPolicyCapability::CAN_INTERACT;
$can_view = PhabricatorPolicyCapability::CAN_VIEW;
$require = array();
// If the object doesn't support a separate "Interact" capability, we
// only use the "View" capability: for most objects, you can interact
// with them if you can see them.
$require[] = $can_view;
if (isset($capabilities[$can_interact])) {
$require[] = $can_interact;
}
foreach ($require as $capability) {
if (!self::hasCapability($user, $object, $capability)) {
return false;
}
}
return true;
}
public function setViewer(PhabricatorUser $user) {
$this->viewer = $user;
return $this;
}
public function requireCapabilities(array $capabilities) {
$this->capabilities = $capabilities;
return $this;
}
public function raisePolicyExceptions($raise) {
$this->raisePolicyExceptions = $raise;
return $this;
}
public function forcePolicy($forced_policy) {
$this->forcedPolicy = $forced_policy;
return $this;
}
public function apply(array $objects) {
assert_instances_of($objects, 'PhabricatorPolicyInterface');
$viewer = $this->viewer;
$capabilities = $this->capabilities;
if (!$viewer || !$capabilities) {
throw new PhutilInvalidStateException('setViewer', 'requireCapabilities');
}
// If the viewer is omnipotent, short circuit all the checks and just
// return the input unmodified. This is an optimization; we know the
// result already.
if ($viewer->isOmnipotent()) {
return $objects;
}
// Before doing any actual object checks, make sure the viewer can see
// the applications that these objects belong to. This is normally enforced
// in the Query layer before we reach object filtering, but execution
// sometimes reaches policy filtering without running application checks.
$objects = $this->applyApplicationChecks($objects);
$filtered = array();
$viewer_phid = $viewer->getPHID();
if (empty($this->userProjects[$viewer_phid])) {
$this->userProjects[$viewer_phid] = array();
}
$need_projects = array();
$need_policies = array();
$need_objpolicies = array();
foreach ($objects as $key => $object) {
$object_capabilities = $object->getCapabilities();
foreach ($capabilities as $capability) {
if (!in_array($capability, $object_capabilities)) {
throw new Exception(
pht(
- "Testing for capability '%s' on an object which does ".
- "not have that capability!",
- $capability));
+ 'Testing for capability "%s" on an object ("%s") which does '.
+ 'not support that capability.',
+ $capability,
+ get_class($object)));
}
$policy = $this->getObjectPolicy($object, $capability);
if (PhabricatorPolicyQuery::isObjectPolicy($policy)) {
$need_objpolicies[$policy][] = $object;
continue;
}
$type = phid_get_type($policy);
if ($type == PhabricatorProjectProjectPHIDType::TYPECONST) {
$need_projects[$policy] = $policy;
continue;
}
if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) {
$need_policies[$policy][] = $object;
continue;
}
}
}
if ($need_objpolicies) {
$this->loadObjectPolicies($need_objpolicies);
}
if ($need_policies) {
$this->loadCustomPolicies($need_policies);
}
// If we need projects, check if any of the projects we need are also the
// objects we're filtering. Because of how project rules work, this is a
// common case.
if ($need_projects) {
foreach ($objects as $object) {
if ($object instanceof PhabricatorProject) {
$project_phid = $object->getPHID();
if (isset($need_projects[$project_phid])) {
$is_member = $object->isUserMember($viewer_phid);
$this->userProjects[$viewer_phid][$project_phid] = $is_member;
unset($need_projects[$project_phid]);
}
}
}
}
if ($need_projects) {
$need_projects = array_unique($need_projects);
// NOTE: We're using the omnipotent user here to avoid a recursive
// descent into madness. We don't actually need to know if the user can
// see these projects or not, since: the check is "user is member of
// project", not "user can see project"; and membership implies
// visibility anyway. Without this, we may load other projects and
// re-enter the policy filter and generally create a huge mess.
$projects = id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withMemberPHIDs(array($viewer->getPHID()))
->withPHIDs($need_projects)
->execute();
foreach ($projects as $project) {
$this->userProjects[$viewer_phid][$project->getPHID()] = true;
}
}
foreach ($objects as $key => $object) {
foreach ($capabilities as $capability) {
if (!$this->checkCapability($object, $capability)) {
// If we're missing any capability, move on to the next object.
continue 2;
}
}
// If we make it here, we have all of the required capabilities.
$filtered[$key] = $object;
}
// If we survived the primary checks, apply extended checks to objects
// with extended policies.
$results = array();
$extended = array();
foreach ($filtered as $key => $object) {
if ($object instanceof PhabricatorExtendedPolicyInterface) {
$extended[$key] = $object;
} else {
$results[$key] = $object;
}
}
if ($extended) {
$results += $this->applyExtendedPolicyChecks($extended);
// Put results back in the original order.
$results = array_select_keys($results, array_keys($filtered));
}
return $results;
}
private function applyExtendedPolicyChecks(array $extended_objects) {
$viewer = $this->viewer;
$filter_capabilities = $this->capabilities;
// Iterate over the objects we need to filter and pull all the nonempty
// policies into a flat, structured list.
$all_structs = array();
foreach ($extended_objects as $key => $extended_object) {
foreach ($filter_capabilities as $extended_capability) {
$extended_policies = $extended_object->getExtendedPolicy(
$extended_capability,
$viewer);
if (!$extended_policies) {
continue;
}
foreach ($extended_policies as $extended_policy) {
list($object, $capabilities) = $extended_policy;
// Build a description of the capabilities we need to check. This
// will be something like `"view"`, or `"edit view"`, or possibly
// a longer string with custom capabilities. Later, group the objects
// up into groups which need the same capabilities tested.
$capabilities = (array)$capabilities;
$capabilities = array_fuse($capabilities);
ksort($capabilities);
$group = implode(' ', $capabilities);
$struct = array(
'key' => $key,
'for' => $extended_capability,
'object' => $object,
'capabilities' => $capabilities,
'group' => $group,
);
$all_structs[] = $struct;
}
}
}
// Extract any bare PHIDs from the structs; we need to load these objects.
// These are objects which are required in order to perform an extended
// policy check but which the original viewer did not have permission to
// see (they presumably had other permissions which let them load the
// object in the first place).
$all_phids = array();
foreach ($all_structs as $idx => $struct) {
$object = $struct['object'];
if (is_string($object)) {
$all_phids[$object] = $object;
}
}
// If we have some bare PHIDs, we need to load the corresponding objects.
if ($all_phids) {
// We can pull these with the omnipotent user because we're immediately
// filtering them.
$ref_objects = id(new PhabricatorObjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($all_phids)
->execute();
$ref_objects = mpull($ref_objects, null, 'getPHID');
} else {
$ref_objects = array();
}
// Group the list of checks by the capabilities we need to check.
$groups = igroup($all_structs, 'group');
foreach ($groups as $structs) {
$head = head($structs);
// All of the items in each group are checking for the same capabilities.
$capabilities = $head['capabilities'];
$key_map = array();
$objects_in = array();
foreach ($structs as $struct) {
$extended_key = $struct['key'];
if (empty($extended_objects[$extended_key])) {
// If this object has already been rejected by an earlier filtering
// pass, we don't need to do any tests on it.
continue;
}
$object = $struct['object'];
if (is_string($object)) {
// This is really a PHID, so look it up.
$object_phid = $object;
if (empty($ref_objects[$object_phid])) {
// We weren't able to load the corresponding object, so just
// reject this result outright.
$reject = $extended_objects[$extended_key];
unset($extended_objects[$extended_key]);
// TODO: This could be friendlier.
$this->rejectObject($reject, false, '<bad-ref>');
continue;
}
$object = $ref_objects[$object_phid];
}
$phid = $object->getPHID();
$key_map[$phid][] = $extended_key;
$objects_in[$phid] = $object;
}
if ($objects_in) {
$objects_out = $this->executeExtendedPolicyChecks(
$viewer,
$capabilities,
$objects_in,
$key_map);
$objects_out = mpull($objects_out, null, 'getPHID');
} else {
$objects_out = array();
}
// If any objects were removed by filtering, we're going to reject all
// of the original objects which needed them.
foreach ($objects_in as $phid => $object_in) {
if (isset($objects_out[$phid])) {
// This object survived filtering, so we don't need to throw any
// results away.
continue;
}
foreach ($key_map[$phid] as $extended_key) {
if (empty($extended_objects[$extended_key])) {
// We've already rejected this object, so we don't need to reject
// it again.
continue;
}
$reject = $extended_objects[$extended_key];
unset($extended_objects[$extended_key]);
// It's possible that we're rejecting this object for multiple
// capability/policy failures, but just pick the first one to show
// to the user.
$first_capability = head($capabilities);
$first_policy = $object_in->getPolicy($first_capability);
$this->rejectObject($reject, $first_policy, $first_capability);
}
}
}
return $extended_objects;
}
private function executeExtendedPolicyChecks(
PhabricatorUser $viewer,
array $capabilities,
array $objects,
array $key_map) {
// Do crude cycle detection by seeing if we have a huge stack depth.
// Although more sophisticated cycle detection is possible in theory,
// it is difficult with hierarchical objects like subprojects. Many other
// checks make it difficult to create cycles normally, so just do a
// simple check here to limit damage.
static $depth;
$depth++;
if ($depth > 32) {
foreach ($objects as $key => $object) {
$this->rejectObject($objects[$key], false, '<cycle>');
unset($objects[$key]);
continue;
}
}
if (!$objects) {
return array();
}
$caught = null;
try {
$result = id(new PhabricatorPolicyFilter())
->setViewer($viewer)
->requireCapabilities($capabilities)
->apply($objects);
} catch (Exception $ex) {
$caught = $ex;
}
$depth--;
if ($caught) {
throw $caught;
}
return $result;
}
private function checkCapability(
PhabricatorPolicyInterface $object,
$capability) {
$policy = $this->getObjectPolicy($object, $capability);
if (!$policy) {
// TODO: Formalize this somehow?
$policy = PhabricatorPolicies::POLICY_USER;
}
if ($policy == PhabricatorPolicies::POLICY_PUBLIC) {
// If the object is set to "public" but that policy is disabled for this
// install, restrict the policy to "user".
if (!PhabricatorEnv::getEnvConfig('policy.allow-public')) {
$policy = PhabricatorPolicies::POLICY_USER;
}
// If the object is set to "public" but the capability is not a public
// capability, restrict the policy to "user".
$capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability);
if (!$capobj || !$capobj->shouldAllowPublicPolicySetting()) {
$policy = PhabricatorPolicies::POLICY_USER;
}
}
$viewer = $this->viewer;
if ($viewer->isOmnipotent()) {
return true;
}
if ($object instanceof PhabricatorSpacesInterface) {
$space_phid = $object->getSpacePHID();
if (!$this->canViewerSeeObjectsInSpace($viewer, $space_phid)) {
$this->rejectObjectFromSpace($object, $space_phid);
return false;
}
}
if ($object->hasAutomaticCapability($capability, $viewer)) {
return true;
}
switch ($policy) {
case PhabricatorPolicies::POLICY_PUBLIC:
return true;
case PhabricatorPolicies::POLICY_USER:
if ($viewer->getPHID()) {
return true;
} else {
$this->rejectObject($object, $policy, $capability);
}
break;
case PhabricatorPolicies::POLICY_ADMIN:
if ($viewer->getIsAdmin()) {
return true;
} else {
$this->rejectObject($object, $policy, $capability);
}
break;
case PhabricatorPolicies::POLICY_NOONE:
$this->rejectObject($object, $policy, $capability);
break;
default:
if (PhabricatorPolicyQuery::isObjectPolicy($policy)) {
if ($this->checkObjectPolicy($policy, $object)) {
return true;
} else {
$this->rejectObject($object, $policy, $capability);
break;
}
}
$type = phid_get_type($policy);
if ($type == PhabricatorProjectProjectPHIDType::TYPECONST) {
if (!empty($this->userProjects[$viewer->getPHID()][$policy])) {
return true;
} else {
$this->rejectObject($object, $policy, $capability);
}
} else if ($type == PhabricatorPeopleUserPHIDType::TYPECONST) {
if ($viewer->getPHID() == $policy) {
return true;
} else {
$this->rejectObject($object, $policy, $capability);
}
} else if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) {
if ($this->checkCustomPolicy($policy, $object)) {
return true;
} else {
$this->rejectObject($object, $policy, $capability);
}
} else {
// Reject objects with unknown policies.
$this->rejectObject($object, false, $capability);
}
}
return false;
}
public function rejectObject(
PhabricatorPolicyInterface $object,
$policy,
$capability) {
if (!$this->raisePolicyExceptions) {
return;
}
if ($this->viewer->isOmnipotent()) {
// Never raise policy exceptions for the omnipotent viewer. Although we
// will never normally issue a policy rejection for the omnipotent
// viewer, we can end up here when queries blanket reject objects that
// have failed to load, without distinguishing between nonexistent and
// nonvisible objects.
return;
}
$capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability);
$rejection = null;
if ($capobj) {
$rejection = $capobj->describeCapabilityRejection();
$capability_name = $capobj->getCapabilityName();
} else {
$capability_name = $capability;
}
if (!$rejection) {
// We couldn't find the capability object, or it doesn't provide a
// tailored rejection string.
$rejection = pht(
'You do not have the required capability ("%s") to do whatever you '.
'are trying to do.',
$capability);
}
$more = PhabricatorPolicy::getPolicyExplanation($this->viewer, $policy);
$more = (array)$more;
$more = array_filter($more);
$exceptions = PhabricatorPolicy::getSpecialRules(
$object,
$this->viewer,
$capability,
true);
$details = array_filter(array_merge($more, $exceptions));
$access_denied = $this->renderAccessDenied($object);
$full_message = pht(
'[%s] (%s) %s // %s',
$access_denied,
$capability_name,
$rejection,
implode(' ', $details));
$exception = id(new PhabricatorPolicyException($full_message))
->setTitle($access_denied)
->setObjectPHID($object->getPHID())
->setRejection($rejection)
->setCapability($capability)
->setCapabilityName($capability_name)
->setMoreInfo($details);
throw $exception;
}
private function loadObjectPolicies(array $map) {
$viewer = $this->viewer;
$viewer_phid = $viewer->getPHID();
$rules = PhabricatorPolicyQuery::getObjectPolicyRules(null);
// Make sure we have clean, empty policy rule objects.
foreach ($rules as $key => $rule) {
$rules[$key] = clone $rule;
}
$results = array();
foreach ($map as $key => $object_list) {
$rule = idx($rules, $key);
if (!$rule) {
continue;
}
foreach ($object_list as $object_key => $object) {
if (!$rule->canApplyToObject($object)) {
unset($object_list[$object_key]);
}
}
$rule->willApplyRules($viewer, array(), $object_list);
$results[$key] = $rule;
}
$this->objectPolicies[$viewer_phid] = $results;
}
private function loadCustomPolicies(array $map) {
$viewer = $this->viewer;
$viewer_phid = $viewer->getPHID();
$custom_policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->withPHIDs(array_keys($map))
->execute();
$custom_policies = mpull($custom_policies, null, 'getPHID');
$classes = array();
$values = array();
$objects = array();
foreach ($custom_policies as $policy_phid => $policy) {
foreach ($policy->getCustomRuleClasses() as $class) {
$classes[$class] = $class;
$values[$class][] = $policy->getCustomRuleValues($class);
foreach (idx($map, $policy_phid, array()) as $object) {
$objects[$class][] = $object;
}
}
}
foreach ($classes as $class => $ignored) {
$rule_object = newv($class, array());
// Filter out any objects which the rule can't apply to.
$target_objects = idx($objects, $class, array());
foreach ($target_objects as $key => $target_object) {
if (!$rule_object->canApplyToObject($target_object)) {
unset($target_objects[$key]);
}
}
$rule_object->willApplyRules(
$viewer,
array_mergev($values[$class]),
$target_objects);
$classes[$class] = $rule_object;
}
foreach ($custom_policies as $policy) {
$policy->attachRuleObjects($classes);
}
if (empty($this->customPolicies[$viewer_phid])) {
$this->customPolicies[$viewer_phid] = array();
}
$this->customPolicies[$viewer->getPHID()] += $custom_policies;
}
private function checkObjectPolicy(
$policy_phid,
PhabricatorPolicyInterface $object) {
$viewer = $this->viewer;
$viewer_phid = $viewer->getPHID();
$rule = idx($this->objectPolicies[$viewer_phid], $policy_phid);
if (!$rule) {
return false;
}
if (!$rule->canApplyToObject($object)) {
return false;
}
return $rule->applyRule($viewer, null, $object);
}
private function checkCustomPolicy(
$policy_phid,
PhabricatorPolicyInterface $object) {
$viewer = $this->viewer;
$viewer_phid = $viewer->getPHID();
$policy = idx($this->customPolicies[$viewer_phid], $policy_phid);
if (!$policy) {
// Reject, this policy is bogus.
return false;
}
$objects = $policy->getRuleObjects();
$action = null;
foreach ($policy->getRules() as $rule) {
if (!is_array($rule)) {
// Reject, this policy rule is invalid.
return false;
}
$rule_object = idx($objects, idx($rule, 'rule'));
if (!$rule_object) {
// Reject, this policy has a bogus rule.
return false;
}
if (!$rule_object->canApplyToObject($object)) {
// Reject, this policy rule can't be applied to the given object.
return false;
}
// If the user matches this rule, use this action.
if ($rule_object->applyRule($viewer, idx($rule, 'value'), $object)) {
$action = idx($rule, 'action');
break;
}
}
if ($action === null) {
$action = $policy->getDefaultAction();
}
if ($action === PhabricatorPolicy::ACTION_ALLOW) {
return true;
}
return false;
}
private function getObjectPolicy(
PhabricatorPolicyInterface $object,
$capability) {
if ($this->forcedPolicy) {
return $this->forcedPolicy;
} else {
return $object->getPolicy($capability);
}
}
private function renderAccessDenied(PhabricatorPolicyInterface $object) {
// NOTE: Not every type of policy object has a real PHID; just load an
// empty handle if a real PHID isn't available.
$phid = nonempty($object->getPHID(), PhabricatorPHIDConstants::PHID_VOID);
$handle = id(new PhabricatorHandleQuery())
->setViewer($this->viewer)
->withPHIDs(array($phid))
->executeOne();
$object_name = $handle->getObjectName();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
if ($is_serious) {
$access_denied = pht(
'Access Denied: %s',
$object_name);
} else {
$access_denied = pht(
'You Shall Not Pass: %s',
$object_name);
}
return $access_denied;
}
private function canViewerSeeObjectsInSpace(
PhabricatorUser $viewer,
$space_phid) {
$spaces = PhabricatorSpacesNamespaceQuery::getAllSpaces();
// If there are no spaces, everything exists in an implicit default space
// with no policy controls. This is the default state.
if (!$spaces) {
if ($space_phid !== null) {
return false;
} else {
return true;
}
}
if ($space_phid === null) {
$space = PhabricatorSpacesNamespaceQuery::getDefaultSpace();
} else {
$space = idx($spaces, $space_phid);
}
if (!$space) {
return false;
}
// This may be more involved later, but for now being able to see the
// space is equivalent to being able to see everything in it.
return self::hasCapability(
$viewer,
$space,
PhabricatorPolicyCapability::CAN_VIEW);
}
private function rejectObjectFromSpace(
PhabricatorPolicyInterface $object,
$space_phid) {
if (!$this->raisePolicyExceptions) {
return;
}
if ($this->viewer->isOmnipotent()) {
return;
}
$access_denied = $this->renderAccessDenied($object);
$rejection = pht(
'This object is in a space you do not have permission to access.');
$full_message = pht('[%s] %s', $access_denied, $rejection);
$exception = id(new PhabricatorPolicyException($full_message))
->setTitle($access_denied)
->setObjectPHID($object->getPHID())
->setRejection($rejection)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW);
throw $exception;
}
private function applyApplicationChecks(array $objects) {
$viewer = $this->viewer;
foreach ($objects as $key => $object) {
// Don't filter handles: users are allowed to see handles from an
// application they can't see even if they can not see objects from
// that application. Note that the application policies still apply to
// the underlying object, so these will be "Restricted Object" handles.
// If we don't let these through, PhabricatorHandleQuery will completely
// fail to load results for PHIDs that are part of applications which
// the viewer can not see, but a fundamental property of handles is that
// we always load something and they can safely be assumed to load.
if ($object instanceof PhabricatorObjectHandle) {
continue;
}
$phid = $object->getPHID();
if (!$phid) {
continue;
}
$application_class = $this->getApplicationForPHID($phid);
if ($application_class === null) {
continue;
}
$can_see = PhabricatorApplication::isClassInstalledForViewer(
$application_class,
$viewer);
if ($can_see) {
continue;
}
unset($objects[$key]);
$application = newv($application_class, array());
$this->rejectObject(
$application,
$application->getPolicy(PhabricatorPolicyCapability::CAN_VIEW),
PhabricatorPolicyCapability::CAN_VIEW);
}
return $objects;
}
private function getApplicationForPHID($phid) {
static $class_map = array();
$phid_type = phid_get_type($phid);
if (!isset($class_map[$phid_type])) {
$type_objects = PhabricatorPHIDType::getTypes(array($phid_type));
$type_object = idx($type_objects, $phid_type);
if (!$type_object) {
$class = false;
} else {
$class = $type_object->getPHIDTypeApplicationClass();
}
$class_map[$phid_type] = $class;
}
$class = $class_map[$phid_type];
if ($class === false) {
return null;
}
return $class;
}
}
diff --git a/src/applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php b/src/applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php
index 33f7e209c..64a32b718 100644
--- a/src/applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php
+++ b/src/applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php
@@ -1,132 +1,153 @@
<?php
final class PhabricatorPolicyManagementUnlockWorkflow
extends PhabricatorPolicyManagementWorkflow {
protected function didConstruct() {
$this
->setName('unlock')
->setSynopsis(
pht(
- 'Unlock an object by setting its policies to allow anyone to view '.
- 'and edit it.'))
- ->setExamples('**unlock** D123')
+ 'Unlock an object which has policies that prevent it from being '.
+ 'viewed or edited.'))
+ ->setExamples('**unlock** --view __user__ __object__')
->setArguments(
array(
array(
- 'name' => 'objects',
- 'wildcard' => true,
+ 'name' => 'view',
+ 'param' => 'username',
+ 'help' => pht(
+ 'Change the view policy of an object so that the specified '.
+ 'user may view it.'),
+ ),
+ array(
+ 'name' => 'edit',
+ 'param' => 'username',
+ 'help' => pht(
+ 'Change the edit policy of an object so that the specified '.
+ 'user may edit it.'),
+ ),
+ array(
+ 'name' => 'owner',
+ 'param' => 'username',
+ 'help' => pht(
+ 'Change the owner of an object to the specified user.'),
+ ),
+ array(
+ 'name' => 'objects',
+ 'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
- $console = PhutilConsole::getConsole();
$viewer = $this->getViewer();
- $obj_names = $args->getArg('objects');
- if (!$obj_names) {
+ $object_names = $args->getArg('objects');
+ if (!$object_names) {
throw new PhutilArgumentUsageException(
pht('Specify the name of an object to unlock.'));
- } else if (count($obj_names) > 1) {
+ } else if (count($object_names) > 1) {
throw new PhutilArgumentUsageException(
pht('Specify the name of exactly one object to unlock.'));
}
+ $object_name = head($object_names);
+
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
- ->withNames($obj_names)
+ ->withNames(array($object_name))
->executeOne();
-
if (!$object) {
- $name = head($obj_names);
throw new PhutilArgumentUsageException(
- pht("No such object '%s'!", $name));
+ pht(
+ 'Unable to find any object with the specified name ("%s").',
+ $object_name));
+ }
+
+ $view_user = $this->loadUser($args->getArg('view'));
+ $edit_user = $this->loadUser($args->getArg('edit'));
+ $owner_user = $this->loadUser($args->getArg('owner'));
+
+ if (!$view_user && !$edit_user && !$owner_user) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Choose which capabilities to unlock with "--view", "--edit", '.
+ 'or "--owner".'));
}
$handle = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array($object->getPHID()))
->executeOne();
- if ($object instanceof PhabricatorApplication) {
- $application = $object;
+ echo tsprintf(
+ "<bg:blue>** %s **</bg> %s\n",
+ pht('UNLOCKING'),
+ pht('Unlocking: %s', $handle->getFullName()));
- $console->writeOut(
- "%s\n",
- pht('Unlocking Application: %s', $handle->getFullName()));
+ $engine = PhabricatorUnlockEngine::newUnlockEngineForObject($object);
- // For applications, we can't unlock them in a normal way and don't want
- // to unlock every capability, just view and edit.
- $capabilities = array(
- PhabricatorPolicyCapability::CAN_VIEW,
- PhabricatorPolicyCapability::CAN_EDIT,
- );
-
- $key = 'phabricator.application-settings';
- $config_entry = PhabricatorConfigEntry::loadConfigEntry($key);
- $value = $config_entry->getValue();
-
- foreach ($capabilities as $capability) {
- if ($application->isCapabilityEditable($capability)) {
- unset($value[$application->getPHID()]['policy'][$capability]);
- }
- }
+ $xactions = array();
+ if ($view_user) {
+ $xactions[] = $engine->newUnlockViewTransactions($object, $view_user);
+ }
+ if ($edit_user) {
+ $xactions[] = $engine->newUnlockEditTransactions($object, $edit_user);
+ }
+ if ($owner_user) {
+ $xactions[] = $engine->newUnlockOwnerTransactions($object, $owner_user);
+ }
+ $xactions = array_mergev($xactions);
+
+ $policy_application = new PhabricatorPolicyApplication();
+ $content_source = $this->newContentSource();
+
+ $editor = $object->getApplicationTransactionEditor()
+ ->setActor($viewer)
+ ->setActingAsPHID($policy_application->getPHID())
+ ->setContinueOnMissingFields(true)
+ ->setContinueOnNoEffect(true)
+ ->setContentSource($content_source);
+
+ $editor->applyTransactions($object, $xactions);
+
+ echo tsprintf(
+ "<bg:green>** %s **</bg> %s\n",
+ pht('UNLOCKED'),
+ pht('Modified object policies.'));
+
+ $uri = $handle->getURI();
+ if (strlen($uri)) {
+ echo tsprintf(
+ "\n **%s**: __%s__\n\n",
+ pht('Object URI'),
+ PhabricatorEnv::getURI($uri));
+ }
- $config_entry->setValue($value);
- $config_entry->save();
+ return 0;
+ }
- $console->writeOut("%s\n", pht('Saved application.'));
+ private function loadUser($username) {
+ $viewer = $this->getViewer();
- return 0;
+ if ($username === null) {
+ return null;
}
- $console->writeOut("%s\n", pht('Unlocking: %s', $handle->getFullName()));
-
- $updated = false;
- foreach ($object->getCapabilities() as $capability) {
- switch ($capability) {
- case PhabricatorPolicyCapability::CAN_VIEW:
- try {
- $object->setViewPolicy(PhabricatorPolicies::POLICY_USER);
- $console->writeOut("%s\n", pht('Unlocked view policy.'));
- $updated = true;
- } catch (Exception $ex) {
- $console->writeOut("%s\n", pht('View policy is not mutable.'));
- }
- break;
- case PhabricatorPolicyCapability::CAN_EDIT:
- try {
- $object->setEditPolicy(PhabricatorPolicies::POLICY_USER);
- $console->writeOut("%s\n", pht('Unlocked edit policy.'));
- $updated = true;
- } catch (Exception $ex) {
- $console->writeOut("%s\n", pht('Edit policy is not mutable.'));
- }
- break;
- case PhabricatorPolicyCapability::CAN_JOIN:
- try {
- $object->setJoinPolicy(PhabricatorPolicies::POLICY_USER);
- $console->writeOut("%s\n", pht('Unlocked join policy.'));
- $updated = true;
- } catch (Exception $ex) {
- $console->writeOut("%s\n", pht('Join policy is not mutable.'));
- }
- break;
- }
- }
+ $user = id(new PhabricatorPeopleQuery())
+ ->setViewer($viewer)
+ ->withUsernames(array($username))
+ ->executeOne();
- if ($updated) {
- $object->save();
- $console->writeOut("%s\n", pht('Saved object.'));
- } else {
- $console->writeOut(
- "%s\n",
+ if (!$user) {
+ throw new PhutilArgumentUsageException(
pht(
- 'Object has no mutable policies. Try unlocking parent/container '.
- 'object instead. For example, to gain access to a commit, unlock '.
- 'the repository it belongs to.'));
+ 'No user with username "%s" exists.',
+ $username));
}
+
+ return $user;
}
}
diff --git a/src/applications/ponder/view/PonderAddAnswerView.php b/src/applications/ponder/view/PonderAddAnswerView.php
index 43bfd0d6b..20c52dac8 100644
--- a/src/applications/ponder/view/PonderAddAnswerView.php
+++ b/src/applications/ponder/view/PonderAddAnswerView.php
@@ -1,90 +1,90 @@
<?php
final class PonderAddAnswerView extends AphrontView {
private $question;
private $actionURI;
private $draft;
public function setQuestion($question) {
$this->question = $question;
return $this;
}
public function setActionURI($uri) {
$this->actionURI = $uri;
return $this;
}
public function render() {
$question = $this->question;
$viewer = $this->getViewer();
$authors = mpull($question->getAnswers(), null, 'getAuthorPHID');
if (isset($authors[$viewer->getPHID()])) {
$view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setTitle(pht('Already Answered'))
->appendChild(
pht(
'You have already answered this question. You can not answer '.
'twice, but you can edit your existing answer.'));
return phutil_tag_div('ponder-add-answer-view', $view);
}
$info_panel = null;
if ($question->getStatus() != PonderQuestionStatus::STATUS_OPEN) {
$info_panel = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->appendChild(
pht(
'This question has been marked as closed,
but you can still leave a new answer.'));
}
$box_style = null;
$header = id(new PHUIHeaderView())
->setHeader(pht('New Answer'))
->addClass('ponder-add-answer-header');
$form = new AphrontFormView();
$form
->setViewer($viewer)
->setAction($this->actionURI)
->setWorkflow(true)
->addHiddenInput('question_id', $question->getID())
->appendChild(
id(new PhabricatorRemarkupControl())
->setName('answer')
->setLabel(pht('Answer'))
->setError(true)
->setID('answer-content')
->setViewer($viewer))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Add Answer')));
if (!$viewer->isLoggedIn()) {
$login_href = id(new PhutilURI('/auth/start/'))
- ->setQueryParam('next', '/Q'.$question->getID());
+ ->replaceQueryParam('next', '/Q'.$question->getID());
$form = id(new PHUIFormLayoutView())
->addClass('login-to-participate')
->appendChild(
id(new PHUIButtonView())
->setTag('a')
->setText(pht('Log In to Answer'))
->setHref((string)$login_href));
}
$box = id(new PHUIObjectBoxView())
->appendChild($form)
->setHeaderText('Answer')
->addClass('ponder-add-answer-view');
if ($info_panel) {
$box->setInfoView($info_panel);
}
return array($header, $box);
}
}
diff --git a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
index e50c83ab5..186ac7dea 100644
--- a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
+++ b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
@@ -1,1691 +1,1695 @@
<?php
final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testViewProject() {
$user = $this->createUser();
$user->save();
$user2 = $this->createUser();
$user2->save();
$proj = $this->createProject($user);
$proj = $this->refreshProject($proj, $user, true);
$this->joinProject($proj, $user);
$proj->setViewPolicy(PhabricatorPolicies::POLICY_USER);
$proj->save();
$can_view = PhabricatorPolicyCapability::CAN_VIEW;
// When the view policy is set to "users", any user can see the project.
$this->assertTrue((bool)$this->refreshProject($proj, $user));
$this->assertTrue((bool)$this->refreshProject($proj, $user2));
// When the view policy is set to "no one", members can still see the
// project.
$proj->setViewPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$this->assertTrue((bool)$this->refreshProject($proj, $user));
$this->assertFalse((bool)$this->refreshProject($proj, $user2));
}
public function testApplicationPolicy() {
$user = $this->createUser()
->save();
$proj = $this->createProject($user);
$this->assertTrue(
PhabricatorPolicyFilter::hasCapability(
$user,
$proj,
PhabricatorPolicyCapability::CAN_VIEW));
// This object is visible so its handle should load normally.
$handle = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs(array($proj->getPHID()))
->executeOne();
$this->assertEqual($proj->getPHID(), $handle->getPHID());
// Change the "Can Use Application" policy for Projecs to "No One". This
// should cause filtering checks to fail even when they are executed
// directly rather than via a Query.
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig(
'phabricator.application-settings',
array(
'PHID-APPS-PhabricatorProjectApplication' => array(
'policy' => array(
'view' => PhabricatorPolicies::POLICY_NOONE,
),
),
));
// Application visibility is cached because it does not normally change
// over the course of a single request. Drop the cache so the next filter
// test uses the new visibility.
PhabricatorCaches::destroyRequestCache();
$this->assertFalse(
PhabricatorPolicyFilter::hasCapability(
$user,
$proj,
PhabricatorPolicyCapability::CAN_VIEW));
// We should still be able to load a handle for the project, even if we
// can not see the application.
$handle = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs(array($proj->getPHID()))
->executeOne();
// The handle should load...
$this->assertEqual($proj->getPHID(), $handle->getPHID());
// ...but be policy filtered.
$this->assertTrue($handle->getPolicyFiltered());
unset($env);
}
public function testIsViewerMemberOrWatcher() {
$user1 = $this->createUser()
->save();
$user2 = $this->createUser()
->save();
$user3 = $this->createUser()
->save();
$proj1 = $this->createProject($user1);
$proj1 = $this->refreshProject($proj1, $user1);
$this->joinProject($proj1, $user1);
$this->joinProject($proj1, $user3);
$this->watchProject($proj1, $user3);
$proj1 = $this->refreshProject($proj1, $user1);
$this->assertTrue($proj1->isUserMember($user1->getPHID()));
$proj1 = $this->refreshProject($proj1, $user1, false, true);
$this->assertTrue($proj1->isUserMember($user1->getPHID()));
$this->assertFalse($proj1->isUserWatcher($user1->getPHID()));
$proj1 = $this->refreshProject($proj1, $user1, true, false);
$this->assertTrue($proj1->isUserMember($user1->getPHID()));
$this->assertFalse($proj1->isUserMember($user2->getPHID()));
$this->assertTrue($proj1->isUserMember($user3->getPHID()));
$proj1 = $this->refreshProject($proj1, $user1, true, true);
$this->assertTrue($proj1->isUserMember($user1->getPHID()));
$this->assertFalse($proj1->isUserMember($user2->getPHID()));
$this->assertTrue($proj1->isUserMember($user3->getPHID()));
$this->assertFalse($proj1->isUserWatcher($user1->getPHID()));
$this->assertFalse($proj1->isUserWatcher($user2->getPHID()));
$this->assertTrue($proj1->isUserWatcher($user3->getPHID()));
}
public function testEditProject() {
$user = $this->createUser();
$user->save();
$user->setAllowInlineCacheGeneration(true);
$proj = $this->createProject($user);
// When edit and view policies are set to "user", anyone can edit.
$proj->setViewPolicy(PhabricatorPolicies::POLICY_USER);
$proj->setEditPolicy(PhabricatorPolicies::POLICY_USER);
$proj->save();
$this->assertTrue($this->attemptProjectEdit($proj, $user));
// When edit policy is set to "no one", no one can edit.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$caught = null;
try {
$this->attemptProjectEdit($proj, $user);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($caught instanceof Exception);
}
public function testAncestorMembers() {
$user1 = $this->createUser();
$user1->save();
$user2 = $this->createUser();
$user2->save();
$parent = $this->createProject($user1);
$child = $this->createProject($user1, $parent);
$this->joinProject($child, $user1);
$this->joinProject($child, $user2);
$project = id(new PhabricatorProjectQuery())
->setViewer($user1)
->withPHIDs(array($child->getPHID()))
->needAncestorMembers(true)
->executeOne();
$members = array_fuse($project->getParentProject()->getMemberPHIDs());
ksort($members);
$expect = array_fuse(
array(
$user1->getPHID(),
$user2->getPHID(),
));
ksort($expect);
$this->assertEqual($expect, $members);
}
public function testAncestryQueries() {
$user = $this->createUser();
$user->save();
$ancestor = $this->createProject($user);
$parent = $this->createProject($user, $ancestor);
$child = $this->createProject($user, $parent);
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->execute();
$this->assertEqual(2, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withParentProjectPHIDs(array($ancestor->getPHID()))
->execute();
$this->assertEqual(1, count($projects));
$this->assertEqual(
$parent->getPHID(),
head($projects)->getPHID());
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->withDepthBetween(2, null)
->execute();
$this->assertEqual(1, count($projects));
$this->assertEqual(
$child->getPHID(),
head($projects)->getPHID());
$parent2 = $this->createProject($user, $ancestor);
$child2 = $this->createProject($user, $parent2);
$grandchild2 = $this->createProject($user, $child2);
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->execute();
$this->assertEqual(5, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withParentProjectPHIDs(array($ancestor->getPHID()))
->execute();
$this->assertEqual(2, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->withDepthBetween(2, null)
->execute();
$this->assertEqual(3, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->withDepthBetween(3, null)
->execute();
$this->assertEqual(1, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(
array(
$child->getPHID(),
$grandchild2->getPHID(),
))
->execute();
$this->assertEqual(2, count($projects));
}
public function testMemberMaterialization() {
$material_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST;
$user = $this->createUser();
$user->save();
$parent = $this->createProject($user);
$child = $this->createProject($user, $parent);
$this->joinProject($child, $user);
$parent_material = PhabricatorEdgeQuery::loadDestinationPHIDs(
$parent->getPHID(),
$material_type);
$this->assertEqual(
array($user->getPHID()),
$parent_material);
}
public function testMilestones() {
$user = $this->createUser();
$user->save();
$parent = $this->createProject($user);
$m1 = $this->createProject($user, $parent, true);
$m2 = $this->createProject($user, $parent, true);
$m3 = $this->createProject($user, $parent, true);
$this->assertEqual(1, $m1->getMilestoneNumber());
$this->assertEqual(2, $m2->getMilestoneNumber());
$this->assertEqual(3, $m3->getMilestoneNumber());
}
public function testMilestoneMembership() {
$user = $this->createUser();
$user->save();
$parent = $this->createProject($user);
$milestone = $this->createProject($user, $parent, true);
$this->joinProject($parent, $user);
$milestone = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($milestone->getPHID()))
->executeOne();
$this->assertTrue($milestone->isUserMember($user->getPHID()));
$milestone = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($milestone->getPHID()))
->needMembers(true)
->executeOne();
$this->assertEqual(
array($user->getPHID()),
$milestone->getMemberPHIDs());
}
public function testSameSlugAsName() {
// It should be OK to type the primary hashtag into "additional hashtags",
// even if the primary hashtag doesn't exist yet because you're creating
// or renaming the project.
$user = $this->createUser();
$user->save();
$project = $this->createProject($user);
// In this first case, set the name and slugs at the same time.
$name = 'slugproject';
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectNameTransaction::TRANSACTIONTYPE)
->setNewValue($name);
$this->applyTransactions($project, $user, $xactions);
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($name));
$this->applyTransactions($project, $user, $xactions);
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($project->getPHID()))
->needSlugs(true)
->executeOne();
$slugs = $project->getSlugs();
$slugs = mpull($slugs, 'getSlug');
$this->assertTrue(in_array($name, $slugs));
// In this second case, set the name first and then the slugs separately.
$name2 = 'slugproject2';
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectNameTransaction::TRANSACTIONTYPE)
->setNewValue($name2);
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($name2));
$this->applyTransactions($project, $user, $xactions);
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($project->getPHID()))
->needSlugs(true)
->executeOne();
$slugs = $project->getSlugs();
$slugs = mpull($slugs, 'getSlug');
$this->assertTrue(in_array($name2, $slugs));
}
public function testDuplicateSlugs() {
// Creating a project with multiple duplicate slugs should succeed.
$user = $this->createUser();
$user->save();
$project = $this->createProject($user);
$input = 'duplicate';
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($input, $input));
$this->applyTransactions($project, $user, $xactions);
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($project->getPHID()))
->needSlugs(true)
->executeOne();
$slugs = $project->getSlugs();
$slugs = mpull($slugs, 'getSlug');
$this->assertTrue(in_array($input, $slugs));
}
public function testNormalizeSlugs() {
// When a user creates a project with slug "XxX360n0sc0perXxX", normalize
// it before writing it.
$user = $this->createUser();
$user->save();
$project = $this->createProject($user);
$input = 'NoRmAlIzE';
$expect = 'normalize';
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($input));
$this->applyTransactions($project, $user, $xactions);
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($project->getPHID()))
->needSlugs(true)
->executeOne();
$slugs = $project->getSlugs();
$slugs = mpull($slugs, 'getSlug');
$this->assertTrue(in_array($expect, $slugs));
// If another user tries to add the same slug in denormalized form, it
// should be caught and fail, even though the database version of the slug
// is normalized.
$project2 = $this->createProject($user);
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($input));
$caught = null;
try {
$this->applyTransactions($project2, $user, $xactions);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$caught = $ex;
}
$this->assertTrue((bool)$caught);
}
public function testProjectMembersVisibility() {
// This is primarily testing that you can create a project and set the
// visibility or edit policy to "Project Members" immediately.
$user1 = $this->createUser();
$user1->save();
$user2 = $this->createUser();
$user2->save();
$project = PhabricatorProject::initializeNewProject($user1);
$name = pht('Test Project %d', mt_rand());
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectNameTransaction::TRANSACTIONTYPE)
->setNewValue($name);
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue(
id(new PhabricatorProjectMembersPolicyRule())
->getObjectPolicyFullKey());
$edge_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_type)
->setNewValue(
array(
'=' => array($user1->getPHID() => $user1->getPHID()),
));
$this->applyTransactions($project, $user1, $xactions);
$this->assertTrue((bool)$this->refreshProject($project, $user1));
$this->assertFalse((bool)$this->refreshProject($project, $user2));
$this->leaveProject($project, $user1);
$this->assertFalse((bool)$this->refreshProject($project, $user1));
}
public function testParentProject() {
$user = $this->createUser();
$user->save();
$parent = $this->createProject($user);
$child = $this->createProject($user, $parent);
$this->assertTrue(true);
$child = $this->refreshProject($child, $user);
$this->assertEqual(
$parent->getPHID(),
$child->getParentProject()->getPHID());
$this->assertEqual(1, (int)$child->getProjectDepth());
$this->assertFalse(
$child->isUserMember($user->getPHID()));
$this->assertFalse(
$child->getParentProject()->isUserMember($user->getPHID()));
$this->joinProject($child, $user);
$child = $this->refreshProject($child, $user);
$this->assertTrue(
$child->isUserMember($user->getPHID()));
$this->assertTrue(
$child->getParentProject()->isUserMember($user->getPHID()));
// Test that hiding a parent hides the child.
$user2 = $this->createUser();
$user2->save();
// Second user can see the project for now.
$this->assertTrue((bool)$this->refreshProject($child, $user2));
// Hide the parent.
$this->setViewPolicy($parent, $user, $user->getPHID());
// First user (who can see the parent because they are a member of
// the child) can see the project.
$this->assertTrue((bool)$this->refreshProject($child, $user));
// Second user can not, because they can't see the parent.
$this->assertFalse((bool)$this->refreshProject($child, $user2));
}
public function testSlugMaps() {
// When querying by slugs, slugs should be normalized and the mapping
// should be reported correctly.
$user = $this->createUser();
$user->save();
$name = 'queryslugproject';
$name2 = 'QUERYslugPROJECT';
$slug = 'queryslugextra';
$slug2 = 'QuErYSlUgExTrA';
$project = PhabricatorProject::initializeNewProject($user);
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectNameTransaction::TRANSACTIONTYPE)
->setNewValue($name);
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($slug));
$this->applyTransactions($project, $user, $xactions);
$project_query = id(new PhabricatorProjectQuery())
->setViewer($user)
->withSlugs(array($name));
$project_query->execute();
$map = $project_query->getSlugMap();
$this->assertEqual(
array(
$name => $project->getPHID(),
),
ipull($map, 'projectPHID'));
$project_query = id(new PhabricatorProjectQuery())
->setViewer($user)
->withSlugs(array($slug));
$project_query->execute();
$map = $project_query->getSlugMap();
$this->assertEqual(
array(
$slug => $project->getPHID(),
),
ipull($map, 'projectPHID'));
$project_query = id(new PhabricatorProjectQuery())
->setViewer($user)
->withSlugs(array($name, $slug, $name2, $slug2));
$project_query->execute();
$map = $project_query->getSlugMap();
$expect = array(
$name => $project->getPHID(),
$slug => $project->getPHID(),
$name2 => $project->getPHID(),
$slug2 => $project->getPHID(),
);
$actual = ipull($map, 'projectPHID');
ksort($expect);
ksort($actual);
$this->assertEqual($expect, $actual);
$expect = array(
$name => $name,
$slug => $slug,
$name2 => $name,
$slug2 => $slug,
);
$actual = ipull($map, 'slug');
ksort($expect);
ksort($actual);
$this->assertEqual($expect, $actual);
}
public function testJoinLeaveProject() {
$user = $this->createUser();
$user->save();
$proj = $this->createProjectWithNewAuthor();
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue(
(bool)$proj,
pht(
'Assumption that projects are default visible '.
'to any user when created.'));
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
pht('Arbitrary user not member of project.'));
// Join the project.
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
pht('Join works.'));
// Join the project again.
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
pht('Joining an already-joined project is a no-op.'));
// Leave the project.
$this->leaveProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
pht('Leave works.'));
// Leave the project again.
$this->leaveProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
pht('Leaving an already-left project is a no-op.'));
// If a user can't edit or join a project, joining fails.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$caught = null;
try {
$this->joinProject($proj, $user);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($ex instanceof Exception);
// If a user can edit a project, they can join.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_USER);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
pht('Join allowed with edit permission.'));
$this->leaveProject($proj, $user);
// If a user can join a project, they can join, even if they can't edit.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_USER);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
pht('Join allowed with join permission.'));
// A user can leave a project even if they can't edit it or join.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$this->leaveProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
pht('Leave allowed without any permission.'));
}
public function testComplexConstraints() {
$user = $this->createUser();
$user->save();
$engineering = $this->createProject($user);
$engineering_scan = $this->createProject($user, $engineering);
$engineering_warp = $this->createProject($user, $engineering);
$exploration = $this->createProject($user);
$exploration_diplomacy = $this->createProject($user, $exploration);
$task_engineering = $this->newTask(
$user,
array($engineering),
pht('Engineering Only'));
$task_exploration = $this->newTask(
$user,
array($exploration),
pht('Exploration Only'));
$task_warp_explore = $this->newTask(
$user,
array($engineering_warp, $exploration),
pht('Warp to New Planet'));
$task_diplomacy_scan = $this->newTask(
$user,
array($engineering_scan, $exploration_diplomacy),
pht('Scan Diplomat'));
$task_diplomacy = $this->newTask(
$user,
array($exploration_diplomacy),
pht('Diplomatic Meeting'));
$task_warp_scan = $this->newTask(
$user,
array($engineering_scan, $engineering_warp),
pht('Scan Warp Drives'));
$this->assertQueryByProjects(
$user,
array(
$task_engineering,
$task_warp_explore,
$task_diplomacy_scan,
$task_warp_scan,
),
array($engineering),
pht('All Engineering'));
$this->assertQueryByProjects(
$user,
array(
$task_diplomacy_scan,
$task_warp_scan,
),
array($engineering_scan),
pht('All Scan'));
$this->assertQueryByProjects(
$user,
array(
$task_warp_explore,
$task_diplomacy_scan,
),
array($engineering, $exploration),
pht('Engineering + Exploration'));
// This is testing that a query for "Parent" and "Parent > Child" works
// properly.
$this->assertQueryByProjects(
$user,
array(
$task_diplomacy_scan,
$task_warp_scan,
),
array($engineering, $engineering_scan),
pht('Engineering + Scan'));
}
public function testTagAncestryConflicts() {
$user = $this->createUser();
$user->save();
$stonework = $this->createProject($user);
$stonework_masonry = $this->createProject($user, $stonework);
$stonework_sculpting = $this->createProject($user, $stonework);
$task = $this->newTask($user, array());
$this->assertEqual(array(), $this->getTaskProjects($task));
$this->addProjectTags($user, $task, array($stonework->getPHID()));
$this->assertEqual(
array(
$stonework->getPHID(),
),
$this->getTaskProjects($task));
// Adding a descendant should remove the parent.
$this->addProjectTags($user, $task, array($stonework_masonry->getPHID()));
$this->assertEqual(
array(
$stonework_masonry->getPHID(),
),
$this->getTaskProjects($task));
// Adding an ancestor should remove the descendant.
$this->addProjectTags($user, $task, array($stonework->getPHID()));
$this->assertEqual(
array(
$stonework->getPHID(),
),
$this->getTaskProjects($task));
// Adding two tags in the same hierarchy which are not mutual ancestors
// should remove the ancestor but otherwise work fine.
$this->addProjectTags(
$user,
$task,
array(
$stonework_masonry->getPHID(),
$stonework_sculpting->getPHID(),
));
$expect = array(
$stonework_masonry->getPHID(),
$stonework_sculpting->getPHID(),
);
sort($expect);
$this->assertEqual($expect, $this->getTaskProjects($task));
}
public function testTagMilestoneConflicts() {
$user = $this->createUser();
$user->save();
$stonework = $this->createProject($user);
$stonework_1 = $this->createProject($user, $stonework, true);
$stonework_2 = $this->createProject($user, $stonework, true);
$task = $this->newTask($user, array());
$this->assertEqual(array(), $this->getTaskProjects($task));
$this->addProjectTags($user, $task, array($stonework->getPHID()));
$this->assertEqual(
array(
$stonework->getPHID(),
),
$this->getTaskProjects($task));
// Adding a milesone should remove the parent.
$this->addProjectTags($user, $task, array($stonework_1->getPHID()));
$this->assertEqual(
array(
$stonework_1->getPHID(),
),
$this->getTaskProjects($task));
// Adding the parent should remove the milestone.
$this->addProjectTags($user, $task, array($stonework->getPHID()));
$this->assertEqual(
array(
$stonework->getPHID(),
),
$this->getTaskProjects($task));
// First, add one milestone.
$this->addProjectTags($user, $task, array($stonework_1->getPHID()));
// Now, adding a second milestone should remove the first milestone.
$this->addProjectTags($user, $task, array($stonework_2->getPHID()));
$this->assertEqual(
array(
$stonework_2->getPHID(),
),
$this->getTaskProjects($task));
}
public function testBoardMoves() {
$user = $this->createUser();
$user->save();
$board = $this->createProject($user);
$backlog = $this->addColumn($user, $board, 0);
$column = $this->addColumn($user, $board, 1);
// New tasks should appear in the backlog.
$task1 = $this->newTask($user, array($board));
$expect = array(
$backlog->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task1);
// Moving a task should move it to the destination column.
$this->moveToColumn($user, $board, $task1, $backlog, $column);
$expect = array(
$column->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task1);
// Same thing again, with a new task.
$task2 = $this->newTask($user, array($board));
$expect = array(
$backlog->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task2);
// Move it, too.
$this->moveToColumn($user, $board, $task2, $backlog, $column);
$expect = array(
$column->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task2);
// Now the stuff should be in the column, in order, with the more recently
// moved task on top.
$expect = array(
$task2->getPHID(),
$task1->getPHID(),
);
- $this->assertTasksInColumn($expect, $user, $board, $column);
+ $label = pht('Simple move');
+ $this->assertTasksInColumn($expect, $user, $board, $column, $label);
// Move the second task after the first task.
$options = array(
- 'afterPHID' => $task1->getPHID(),
+ 'afterPHIDs' => array($task1->getPHID()),
);
$this->moveToColumn($user, $board, $task2, $column, $column, $options);
$expect = array(
$task1->getPHID(),
$task2->getPHID(),
);
- $this->assertTasksInColumn($expect, $user, $board, $column);
+ $label = pht('With afterPHIDs');
+ $this->assertTasksInColumn($expect, $user, $board, $column, $label);
// Move the second task before the first task.
$options = array(
- 'beforePHID' => $task1->getPHID(),
+ 'beforePHIDs' => array($task1->getPHID()),
);
$this->moveToColumn($user, $board, $task2, $column, $column, $options);
$expect = array(
$task2->getPHID(),
$task1->getPHID(),
);
- $this->assertTasksInColumn($expect, $user, $board, $column);
+ $label = pht('With beforePHIDs');
+ $this->assertTasksInColumn($expect, $user, $board, $column, $label);
}
public function testMilestoneMoves() {
$user = $this->createUser();
$user->save();
$board = $this->createProject($user);
$backlog = $this->addColumn($user, $board, 0);
// Create a task into the backlog.
$task = $this->newTask($user, array($board));
$expect = array(
$backlog->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task);
$milestone = $this->createProject($user, $board, true);
$this->addProjectTags($user, $task, array($milestone->getPHID()));
// We just want the side effect of looking at the board: creation of the
// milestone column.
$this->loadColumns($user, $board, $task);
$column = id(new PhabricatorProjectColumnQuery())
->setViewer($user)
->withProjectPHIDs(array($board->getPHID()))
->withProxyPHIDs(array($milestone->getPHID()))
->executeOne();
$this->assertTrue((bool)$column);
// Moving the task to the milestone should have moved it to the milestone
// column.
$expect = array(
$column->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task);
// Move the task within the "Milestone" column. This should not affect
// the projects the task is tagged with. See T10912.
$task_a = $task;
$task_b = $this->newTask($user, array($backlog));
$this->moveToColumn($user, $board, $task_b, $backlog, $column);
$a_options = array(
'beforePHID' => $task_b->getPHID(),
);
$b_options = array(
'beforePHID' => $task_a->getPHID(),
);
$old_projects = $this->getTaskProjects($task);
// Move the target task to the top.
$this->moveToColumn($user, $board, $task_a, $column, $column, $a_options);
$new_projects = $this->getTaskProjects($task_a);
$this->assertEqual($old_projects, $new_projects);
// Move the other task.
$this->moveToColumn($user, $board, $task_b, $column, $column, $b_options);
$new_projects = $this->getTaskProjects($task_a);
$this->assertEqual($old_projects, $new_projects);
// Move the target task again.
$this->moveToColumn($user, $board, $task_a, $column, $column, $a_options);
$new_projects = $this->getTaskProjects($task_a);
$this->assertEqual($old_projects, $new_projects);
// Add the parent project to the task. This should move it out of the
// milestone column and into the parent's backlog.
$this->addProjectTags($user, $task, array($board->getPHID()));
$expect_columns = array(
$backlog->getPHID(),
);
$this->assertColumns($expect_columns, $user, $board, $task);
$new_projects = $this->getTaskProjects($task);
$expect_projects = array(
$board->getPHID(),
);
$this->assertEqual($expect_projects, $new_projects);
}
public function testColumnExtendedPolicies() {
$user = $this->createUser();
$user->save();
$board = $this->createProject($user);
$column = $this->addColumn($user, $board, 0);
// At first, the user should be able to view and edit the column.
$column = $this->refreshColumn($user, $column);
$this->assertTrue((bool)$column);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$column,
PhabricatorPolicyCapability::CAN_EDIT);
$this->assertTrue($can_edit);
// Now, set the project edit policy to "Members of Project". This should
// disable editing.
$members_policy = id(new PhabricatorProjectMembersPolicyRule())
->getObjectPolicyFullKey();
$board->setEditPolicy($members_policy)->save();
$column = $this->refreshColumn($user, $column);
$this->assertTrue((bool)$column);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$column,
PhabricatorPolicyCapability::CAN_EDIT);
$this->assertFalse($can_edit);
// Now, join the project. This should make the column editable again.
$this->joinProject($board, $user);
$column = $this->refreshColumn($user, $column);
$this->assertTrue((bool)$column);
// This test has been failing randomly in a way that doesn't reproduce
// on any host, so add some extra assertions to try to nail it down.
$board = $this->refreshProject($board, $user, true);
$this->assertTrue((bool)$board);
$this->assertTrue($board->isUserMember($user->getPHID()));
$can_view = PhabricatorPolicyFilter::hasCapability(
$user,
$column,
PhabricatorPolicyCapability::CAN_VIEW);
$this->assertTrue($can_view);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$column,
PhabricatorPolicyCapability::CAN_EDIT);
$this->assertTrue($can_edit);
}
public function testProjectPolicyRules() {
$author = $this->generateNewTestUser();
$proj_a = PhabricatorProject::initializeNewProject($author)
->setName('Policy A')
->save();
$proj_b = PhabricatorProject::initializeNewProject($author)
->setName('Policy B')
->save();
$user_none = $this->generateNewTestUser();
$user_any = $this->generateNewTestUser();
$user_all = $this->generateNewTestUser();
$this->joinProject($proj_a, $user_any);
$this->joinProject($proj_a, $user_all);
$this->joinProject($proj_b, $user_all);
$any_policy = id(new PhabricatorPolicy())
->setRules(
array(
array(
'action' => PhabricatorPolicy::ACTION_ALLOW,
'rule' => 'PhabricatorProjectsPolicyRule',
'value' => array(
$proj_a->getPHID(),
$proj_b->getPHID(),
),
),
))
->save();
$all_policy = id(new PhabricatorPolicy())
->setRules(
array(
array(
'action' => PhabricatorPolicy::ACTION_ALLOW,
'rule' => 'PhabricatorProjectsAllPolicyRule',
'value' => array(
$proj_a->getPHID(),
$proj_b->getPHID(),
),
),
))
->save();
$any_task = ManiphestTask::initializeNewTask($author)
->setViewPolicy($any_policy->getPHID())
->save();
$all_task = ManiphestTask::initializeNewTask($author)
->setViewPolicy($all_policy->getPHID())
->save();
$map = array(
array(
pht('Project policy rule; user in no projects'),
$user_none,
false,
false,
),
array(
pht('Project policy rule; user in some projects'),
$user_any,
true,
false,
),
array(
pht('Project policy rule; user in all projects'),
$user_all,
true,
true,
),
);
foreach ($map as $test_case) {
list($label, $user, $expect_any, $expect_all) = $test_case;
$can_any = PhabricatorPolicyFilter::hasCapability(
$user,
$any_task,
PhabricatorPolicyCapability::CAN_VIEW);
$can_all = PhabricatorPolicyFilter::hasCapability(
$user,
$all_task,
PhabricatorPolicyCapability::CAN_VIEW);
$this->assertEqual($expect_any, $can_any, pht('%s / Any', $label));
$this->assertEqual($expect_all, $can_all, pht('%s / All', $label));
}
}
private function moveToColumn(
PhabricatorUser $viewer,
PhabricatorProject $board,
ManiphestTask $task,
PhabricatorProjectColumn $src,
PhabricatorProjectColumn $dst,
$options = null) {
$xactions = array();
if (!$options) {
$options = array();
}
$value = array(
'columnPHID' => $dst->getPHID(),
) + $options;
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS)
->setNewValue(array($value));
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContentSource($this->newContentSource())
->setContinueOnNoEffect(true)
->applyTransactions($task, $xactions);
}
private function assertColumns(
array $expect,
PhabricatorUser $viewer,
PhabricatorProject $board,
ManiphestTask $task) {
$column_phids = $this->loadColumns($viewer, $board, $task);
$this->assertEqual($expect, $column_phids);
}
private function loadColumns(
PhabricatorUser $viewer,
PhabricatorProject $board,
ManiphestTask $task) {
$engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs(array($board->getPHID()))
->setObjectPHIDs(
array(
$task->getPHID(),
))
->executeLayout();
$columns = $engine->getObjectColumns($board->getPHID(), $task->getPHID());
$column_phids = mpull($columns, 'getPHID');
$column_phids = array_values($column_phids);
return $column_phids;
}
private function assertTasksInColumn(
array $expect,
PhabricatorUser $viewer,
PhabricatorProject $board,
- PhabricatorProjectColumn $column) {
+ PhabricatorProjectColumn $column,
+ $label = null) {
$engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs(array($board->getPHID()))
->setObjectPHIDs($expect)
->executeLayout();
$object_phids = $engine->getColumnObjectPHIDs(
$board->getPHID(),
$column->getPHID());
$object_phids = array_values($object_phids);
- $this->assertEqual($expect, $object_phids);
+ $this->assertEqual($expect, $object_phids, $label);
}
private function addColumn(
PhabricatorUser $viewer,
PhabricatorProject $project,
$sequence) {
$project->setHasWorkboard(1)->save();
return PhabricatorProjectColumn::initializeNewColumn($viewer)
->setSequence(0)
->setProperty('isDefault', ($sequence == 0))
->setProjectPHID($project->getPHID())
->save();
}
private function getTaskProjects(ManiphestTask $task) {
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$task->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
sort($project_phids);
return $project_phids;
}
private function attemptProjectEdit(
PhabricatorProject $proj,
PhabricatorUser $user,
$skip_refresh = false) {
$proj = $this->refreshProject($proj, $user, true);
$new_name = $proj->getName().' '.mt_rand();
$params = array(
'objectIdentifier' => $proj->getID(),
'transactions' => array(
array(
'type' => 'name',
'value' => $new_name,
),
),
);
id(new ConduitCall('project.edit', $params))
->setUser($user)
->execute();
return true;
}
private function addProjectTags(
PhabricatorUser $viewer,
ManiphestTask $task,
array $phids) {
$xactions = array();
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
->setNewValue(
array(
'+' => array_fuse($phids),
));
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContentSource($this->newContentSource())
->setContinueOnNoEffect(true)
->applyTransactions($task, $xactions);
}
private function newTask(
PhabricatorUser $viewer,
array $projects,
$name = null) {
$task = ManiphestTask::initializeNewTask($viewer);
if (!strlen($name)) {
$name = pht('Test Task');
}
$xactions = array();
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTaskTitleTransaction::TRANSACTIONTYPE)
->setNewValue($name);
if ($projects) {
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
->setNewValue(
array(
'=' => array_fuse(mpull($projects, 'getPHID')),
));
}
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContentSource($this->newContentSource())
->setContinueOnNoEffect(true)
->applyTransactions($task, $xactions);
return $task;
}
private function assertQueryByProjects(
PhabricatorUser $viewer,
array $expect,
array $projects,
$label = null) {
$datasource = id(new PhabricatorProjectLogicalDatasource())
->setViewer($viewer);
$project_phids = mpull($projects, 'getPHID');
$constraints = $datasource->evaluateTokens($project_phids);
$query = id(new ManiphestTaskQuery())
->setViewer($viewer);
$query->withEdgeLogicConstraints(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$constraints);
$tasks = $query->execute();
$expect_phids = mpull($expect, 'getTitle', 'getPHID');
ksort($expect_phids);
$actual_phids = mpull($tasks, 'getTitle', 'getPHID');
ksort($actual_phids);
$this->assertEqual($expect_phids, $actual_phids, $label);
}
private function refreshProject(
PhabricatorProject $project,
PhabricatorUser $viewer,
$need_members = false,
$need_watchers = false) {
$results = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->needMembers($need_members)
->needWatchers($need_watchers)
->withIDs(array($project->getID()))
->execute();
if ($results) {
return head($results);
} else {
return null;
}
}
private function refreshColumn(
PhabricatorUser $viewer,
PhabricatorProjectColumn $column) {
$results = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withIDs(array($column->getID()))
->execute();
if ($results) {
return head($results);
} else {
return null;
}
}
private function createProject(
PhabricatorUser $user,
PhabricatorProject $parent = null,
$is_milestone = false) {
$project = PhabricatorProject::initializeNewProject($user);
$name = pht('Test Project %d', mt_rand());
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectNameTransaction::TRANSACTIONTYPE)
->setNewValue($name);
if ($parent) {
if ($is_milestone) {
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(
PhabricatorProjectMilestoneTransaction::TRANSACTIONTYPE)
->setNewValue($parent->getPHID());
} else {
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(
PhabricatorProjectParentTransaction::TRANSACTIONTYPE)
->setNewValue($parent->getPHID());
}
}
$this->applyTransactions($project, $user, $xactions);
// Force these values immediately; they are normally updated by the
// index engine.
if ($parent) {
if ($is_milestone) {
$parent->setHasMilestones(1)->save();
} else {
$parent->setHasSubprojects(1)->save();
}
}
return $project;
}
private function setViewPolicy(
PhabricatorProject $project,
PhabricatorUser $user,
$policy) {
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($policy);
$this->applyTransactions($project, $user, $xactions);
return $project;
}
private function createProjectWithNewAuthor() {
$author = $this->createUser();
$author->save();
$project = $this->createProject($author);
return $project;
}
private function createUser() {
$rand = mt_rand();
$user = new PhabricatorUser();
$user->setUsername('unittestuser'.$rand);
$user->setRealName(pht('Unit Test User %d', $rand));
return $user;
}
private function joinProject(
PhabricatorProject $project,
PhabricatorUser $user) {
return $this->joinOrLeaveProject($project, $user, '+');
}
private function leaveProject(
PhabricatorProject $project,
PhabricatorUser $user) {
return $this->joinOrLeaveProject($project, $user, '-');
}
private function watchProject(
PhabricatorProject $project,
PhabricatorUser $user) {
return $this->watchOrUnwatchProject($project, $user, '+');
}
private function unwatchProject(
PhabricatorProject $project,
PhabricatorUser $user) {
return $this->watchOrUnwatchProject($project, $user, '-');
}
private function joinOrLeaveProject(
PhabricatorProject $project,
PhabricatorUser $user,
$operation) {
return $this->applyProjectEdgeTransaction(
$project,
$user,
$operation,
PhabricatorProjectProjectHasMemberEdgeType::EDGECONST);
}
private function watchOrUnwatchProject(
PhabricatorProject $project,
PhabricatorUser $user,
$operation) {
return $this->applyProjectEdgeTransaction(
$project,
$user,
$operation,
PhabricatorObjectHasWatcherEdgeType::EDGECONST);
}
private function applyProjectEdgeTransaction(
PhabricatorProject $project,
PhabricatorUser $user,
$operation,
$edge_type) {
$spec = array(
$operation => array($user->getPHID() => $user->getPHID()),
);
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_type)
->setNewValue($spec);
$this->applyTransactions($project, $user, $xactions);
return $project;
}
private function applyTransactions(
PhabricatorProject $project,
PhabricatorUser $user,
array $xactions) {
$editor = id(new PhabricatorProjectTransactionEditor())
->setActor($user)
->setContentSource($this->newContentSource())
->setContinueOnNoEffect(true)
->applyTransactions($project, $xactions);
}
}
diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php
index 0e1a9f37c..46d7558f5 100644
--- a/src/applications/project/application/PhabricatorProjectApplication.php
+++ b/src/applications/project/application/PhabricatorProjectApplication.php
@@ -1,154 +1,166 @@
<?php
final class PhabricatorProjectApplication extends PhabricatorApplication {
public function getName() {
return pht('Projects');
}
public function getShortDescription() {
return pht('Projects, Tags, and Teams');
}
public function isPinnedByDefault(PhabricatorUser $viewer) {
return true;
}
public function getBaseURI() {
return '/project/';
}
public function getIcon() {
return 'fa-briefcase';
}
public function getFlavorText() {
return pht('Group stuff into big piles.');
}
public function getRemarkupRules() {
return array(
new ProjectRemarkupRule(),
);
}
public function getEventListeners() {
return array(
new PhabricatorProjectUIEventListener(),
);
}
public function getRoutes() {
return array(
'/project/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?' => 'PhabricatorProjectListController',
'filter/(?P<filter>[^/]+)/' => 'PhabricatorProjectListController',
'archive/(?P<id>[1-9]\d*)/'
=> 'PhabricatorProjectArchiveController',
'lock/(?P<id>[1-9]\d*)/'
=> 'PhabricatorProjectLockController',
'members/(?P<id>[1-9]\d*)/'
=> 'PhabricatorProjectMembersViewController',
'members/(?P<id>[1-9]\d*)/add/'
=> 'PhabricatorProjectMembersAddController',
'(?P<type>members|watchers)/(?P<id>[1-9]\d*)/remove/'
=> 'PhabricatorProjectMembersRemoveController',
'profile/(?P<id>[1-9]\d*)/'
=> 'PhabricatorProjectProfileController',
'view/(?P<id>[1-9]\d*)/'
=> 'PhabricatorProjectViewController',
'picture/(?P<id>[1-9]\d*)/'
=> 'PhabricatorProjectEditPictureController',
$this->getEditRoutePattern('edit/')
=> 'PhabricatorProjectEditController',
'(?P<projectID>[1-9]\d*)/item/' => $this->getProfileMenuRouting(
'PhabricatorProjectMenuItemController'),
'subprojects/(?P<id>[1-9]\d*)/'
=> 'PhabricatorProjectSubprojectsController',
'board/(?P<id>[1-9]\d*)/'.
'(?P<filter>filter/)?'.
'(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhabricatorProjectBoardViewController',
'move/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectMoveController',
'cover/' => 'PhabricatorProjectCoverController',
'board/(?P<projectID>[1-9]\d*)/' => array(
'edit/(?:(?P<id>\d+)/)?'
=> 'PhabricatorProjectColumnEditController',
'hide/(?:(?P<id>\d+)/)?'
=> 'PhabricatorProjectColumnHideController',
'column/(?:(?P<id>\d+)/)?'
=> 'PhabricatorProjectColumnDetailController',
'import/'
=> 'PhabricatorProjectBoardImportController',
'reorder/'
=> 'PhabricatorProjectBoardReorderController',
'disable/'
=> 'PhabricatorProjectBoardDisableController',
'manage/'
=> 'PhabricatorProjectBoardManageController',
'background/'
=> 'PhabricatorProjectBoardBackgroundController',
),
+ 'column/' => array(
+ 'remove/(?P<id>\d+)/' =>
+ 'PhabricatorProjectColumnRemoveTriggerController',
+ ),
+ 'trigger/' => array(
+ $this->getQueryRoutePattern() =>
+ 'PhabricatorProjectTriggerListController',
+ '(?P<id>[1-9]\d*)/' =>
+ 'PhabricatorProjectTriggerViewController',
+ $this->getEditRoutePattern('edit/') =>
+ 'PhabricatorProjectTriggerEditController',
+ ),
'update/(?P<id>[1-9]\d*)/(?P<action>[^/]+)/'
=> 'PhabricatorProjectUpdateController',
'manage/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectManageController',
'(?P<action>watch|unwatch)/(?P<id>[1-9]\d*)/'
=> 'PhabricatorProjectWatchController',
'silence/(?P<id>[1-9]\d*)/'
=> 'PhabricatorProjectSilenceController',
'warning/(?P<id>[1-9]\d*)/'
=> 'PhabricatorProjectSubprojectWarningController',
'default/(?P<projectID>[1-9]\d*)/(?P<target>[^/]+)/'
=> 'PhabricatorProjectDefaultController',
),
'/tag/' => array(
'(?P<slug>[^/]+)/' => 'PhabricatorProjectViewController',
'(?P<slug>[^/]+)/board/' => 'PhabricatorProjectBoardViewController',
),
);
}
protected function getCustomCapabilities() {
return array(
ProjectCreateProjectsCapability::CAPABILITY => array(),
ProjectCanLockProjectsCapability::CAPABILITY => array(
'default' => PhabricatorPolicies::POLICY_ADMIN,
),
ProjectDefaultViewCapability::CAPABILITY => array(
'caption' => pht('Default view policy for newly created projects.'),
'template' => PhabricatorProjectProjectPHIDType::TYPECONST,
'capability' => PhabricatorPolicyCapability::CAN_VIEW,
),
ProjectDefaultEditCapability::CAPABILITY => array(
'caption' => pht('Default edit policy for newly created projects.'),
'template' => PhabricatorProjectProjectPHIDType::TYPECONST,
'capability' => PhabricatorPolicyCapability::CAN_EDIT,
),
ProjectDefaultJoinCapability::CAPABILITY => array(
'caption' => pht('Default join policy for newly created projects.'),
'template' => PhabricatorProjectProjectPHIDType::TYPECONST,
'capability' => PhabricatorPolicyCapability::CAN_JOIN,
),
);
}
public function getApplicationSearchDocumentTypes() {
return array(
PhabricatorProjectProjectPHIDType::TYPECONST,
);
}
public function getApplicationOrder() {
return 0.150;
}
public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
return array(
array(
'name' => pht('Projects User Guide'),
'href' => PhabricatorEnv::getDoclink('Projects User Guide'),
),
);
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectBoardBackgroundController.php b/src/applications/project/controller/PhabricatorProjectBoardBackgroundController.php
index b229f59ec..c70c21139 100644
--- a/src/applications/project/controller/PhabricatorProjectBoardBackgroundController.php
+++ b/src/applications/project/controller/PhabricatorProjectBoardBackgroundController.php
@@ -1,170 +1,170 @@
<?php
final class PhabricatorProjectBoardBackgroundController
extends PhabricatorProjectBoardController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$board_id = $request->getURIData('projectID');
$board = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withIDs(array($board_id))
->needImages(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$board) {
return new Aphront404Response();
}
if (!$board->getHasWorkboard()) {
return new Aphront404Response();
}
$this->setProject($board);
$id = $board->getID();
$view_uri = $this->getApplicationURI("board/{$id}/");
$manage_uri = $this->getApplicationURI("board/{$id}/manage/");
if ($request->isFormPost()) {
$background_key = $request->getStr('backgroundKey');
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(
PhabricatorProjectWorkboardBackgroundTransaction::TRANSACTIONTYPE)
->setNewValue($background_key);
id(new PhabricatorProjectTransactionEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($board, $xactions);
return id(new AphrontRedirectResponse())
->setURI($view_uri);
}
$nav = $this->getProfileMenu();
$crumbs = id($this->buildApplicationCrumbs())
- ->addTextCrumb(pht('Workboard'), "/project/board/{$board_id}/")
+ ->addTextCrumb(pht('Workboard'), $board->getWorkboardURI())
->addTextCrumb(pht('Manage'), $manage_uri)
->addTextCrumb(pht('Background Color'));
$form = id(new AphrontFormView())
->setUser($viewer);
$group_info = array(
'basic' => array(
'label' => pht('Basics'),
),
'solid' => array(
'label' => pht('Solid Colors'),
),
'gradient' => array(
'label' => pht('Gradients'),
),
);
$groups = array();
$options = PhabricatorProjectWorkboardBackgroundColor::getOptions();
$option_groups = igroup($options, 'group');
require_celerity_resource('people-profile-css');
require_celerity_resource('phui-workboard-color-css');
Javelin::initBehavior('phabricator-tooltips', array());
foreach ($group_info as $group_key => $spec) {
$buttons = array();
$available_options = idx($option_groups, $group_key, array());
foreach ($available_options as $option) {
$buttons[] = $this->renderOptionButton($option);
}
$form->appendControl(
id(new AphrontFormMarkupControl())
->setLabel($spec['label'])
->setValue($buttons));
}
// NOTE: Each button is its own form, so we can't wrap them in a normal
// form.
$layout_view = $form->buildLayoutView();
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Edit Background Color'))
->appendChild($layout_view);
return $this->newPage()
->setTitle(
array(
pht('Edit Background Color'),
$board->getDisplayName(),
))
->setCrumbs($crumbs)
->setNavigation($nav)
->appendChild($form_box);
}
private function renderOptionButton(array $option) {
$viewer = $this->getViewer();
$icon = idx($option, 'icon');
if ($icon) {
$preview_class = null;
$preview_content = id(new PHUIIconView())
->setIcon($icon, 'lightbluetext');
} else {
$preview_class = 'phui-workboard-'.$option['key'];
$preview_content = null;
}
$preview = phutil_tag(
'div',
array(
'class' => 'phui-workboard-color-preview '.$preview_class,
),
$preview_content);
$button = javelin_tag(
'button',
array(
'class' => 'button-grey profile-image-button',
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $option['name'],
'size' => 300,
),
),
$preview);
$input = phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'backgroundKey',
'value' => $option['key'],
));
return phabricator_form(
$viewer,
array(
'class' => 'profile-image-form',
'method' => 'POST',
),
array(
$button,
$input,
));
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectBoardManageController.php b/src/applications/project/controller/PhabricatorProjectBoardManageController.php
index 5c71dcfb6..21daf2e65 100644
--- a/src/applications/project/controller/PhabricatorProjectBoardManageController.php
+++ b/src/applications/project/controller/PhabricatorProjectBoardManageController.php
@@ -1,142 +1,142 @@
<?php
final class PhabricatorProjectBoardManageController
extends PhabricatorProjectBoardController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$board_id = $request->getURIData('projectID');
$board = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withIDs(array($board_id))
->needImages(true)
->executeOne();
if (!$board) {
return new Aphront404Response();
}
$this->setProject($board);
// Perform layout of no tasks to load and populate the columns in the
// correct order.
$layout_engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs(array($board->getPHID()))
->setObjectPHIDs(array())
->setFetchAllBoards(true)
->executeLayout();
$columns = $layout_engine->getColumns($board->getPHID());
$board_id = $board->getID();
$header = $this->buildHeaderView($board);
$curtain = $this->buildCurtainView($board);
$crumbs = $this->buildApplicationCrumbs();
- $crumbs->addTextCrumb(pht('Workboard'), "/project/board/{$board_id}/");
+ $crumbs->addTextCrumb(pht('Workboard'), $board->getWorkboardURI());
$crumbs->addTextCrumb(pht('Manage'));
$crumbs->setBorder(true);
$nav = $this->getProfileMenu();
$columns_list = $this->buildColumnsList($board, $columns);
require_celerity_resource('project-view-css');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->addClass('project-view-home')
->addClass('project-view-people-home')
->setCurtain($curtain)
->setMainColumn($columns_list);
$title = array(
pht('Manage Workboard'),
$board->getDisplayName(),
);
return $this->newPage()
->setTitle($title)
->setNavigation($nav)
->setCrumbs($crumbs)
->appendChild($view);
}
private function buildHeaderView(PhabricatorProject $board) {
$viewer = $this->getViewer();
$header = id(new PHUIHeaderView())
->setHeader(pht('Workboard: %s', $board->getDisplayName()))
->setUser($viewer);
return $header;
}
private function buildCurtainView(PhabricatorProject $board) {
$viewer = $this->getViewer();
$id = $board->getID();
$curtain = $this->newCurtainView();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$board,
PhabricatorPolicyCapability::CAN_EDIT);
$disable_uri = $this->getApplicationURI("board/{$id}/disable/");
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-ban')
->setName(pht('Disable Workboard'))
->setHref($disable_uri)
->setDisabled(!$can_edit)
->setWorkflow(true));
return $curtain;
}
private function buildColumnsList(
PhabricatorProject $board,
array $columns) {
assert_instances_of($columns, 'PhabricatorProjectColumn');
$board_id = $board->getID();
$view = id(new PHUIObjectItemListView())
->setNoDataString(pht('This board has no columns.'));
foreach ($columns as $column) {
$column_id = $column->getID();
$proxy = $column->getProxy();
if ($proxy && !$proxy->isMilestone()) {
continue;
}
$detail_uri = "/project/board/{$board_id}/column/{$column_id}/";
$item = id(new PHUIObjectItemView())
->setHeader($column->getDisplayName())
->setHref($detail_uri);
if ($column->isHidden()) {
$item->setDisabled(true);
$item->addAttribute(pht('Hidden'));
$item->setImageIcon('fa-columns grey');
} else {
$item->addAttribute(pht('Visible'));
$item->setImageIcon('fa-columns');
}
$view->addItem($item);
}
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Columns'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList($view);
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php
index 15e0c5d07..775ff1b61 100644
--- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php
+++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php
@@ -1,1372 +1,1507 @@
<?php
final class PhabricatorProjectBoardViewController
extends PhabricatorProjectBoardController {
const BATCH_EDIT_ALL = 'all';
private $id;
private $slug;
private $queryKey;
private $sortKey;
private $showHidden;
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$response = $this->loadProject();
if ($response) {
return $response;
}
$project = $this->getProject();
$this->readRequestState();
$board_uri = $this->getApplicationURI('board/'.$project->getID().'/');
$search_engine = id(new ManiphestTaskSearchEngine())
->setViewer($viewer)
->setBaseURI($board_uri)
->setIsBoardView(true);
if ($request->isFormPost()
&& !$request->getBool('initialize')
&& !$request->getStr('move')
&& !$request->getStr('queryColumnID')) {
$saved = $search_engine->buildSavedQueryFromRequest($request);
$search_engine->saveQuery($saved);
$filter_form = id(new AphrontFormView())
->setUser($viewer);
$search_engine->buildSearchForm($filter_form, $saved);
if ($search_engine->getErrors()) {
return $this->newDialog()
->setWidth(AphrontDialogView::WIDTH_FULL)
->setTitle(pht('Advanced Filter'))
->appendChild($filter_form->buildLayoutView())
->setErrors($search_engine->getErrors())
->setSubmitURI($board_uri)
->addSubmitButton(pht('Apply Filter'))
->addCancelButton($board_uri);
}
return id(new AphrontRedirectResponse())->setURI(
$this->getURIWithState(
$search_engine->getQueryResultsPageURI($saved->getQueryKey())));
}
$query_key = $this->getDefaultFilter($project);
$request_query = $request->getStr('filter');
if (strlen($request_query)) {
$query_key = $request_query;
}
$uri_query = $request->getURIData('queryKey');
if (strlen($uri_query)) {
$query_key = $uri_query;
}
$this->queryKey = $query_key;
$custom_query = null;
if ($search_engine->isBuiltinQuery($query_key)) {
$saved = $search_engine->buildSavedQueryFromBuiltin($query_key);
} else {
$saved = id(new PhabricatorSavedQueryQuery())
->setViewer($viewer)
->withQueryKeys(array($query_key))
->executeOne();
if (!$saved) {
return new Aphront404Response();
}
$custom_query = $saved;
}
if ($request->getURIData('filter')) {
$filter_form = id(new AphrontFormView())
->setUser($viewer);
$search_engine->buildSearchForm($filter_form, $saved);
return $this->newDialog()
->setWidth(AphrontDialogView::WIDTH_FULL)
->setTitle(pht('Advanced Filter'))
->appendChild($filter_form->buildLayoutView())
->setSubmitURI($board_uri)
->addSubmitButton(pht('Apply Filter'))
->addCancelButton($board_uri);
}
$task_query = $search_engine->buildQueryFromSavedQuery($saved);
$select_phids = array($project->getPHID());
if ($project->getHasSubprojects() || $project->getHasMilestones()) {
$descendants = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withAncestorProjectPHIDs($select_phids)
->execute();
foreach ($descendants as $descendant) {
$select_phids[] = $descendant->getPHID();
}
}
$tasks = $task_query
->withEdgeLogicPHIDs(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
PhabricatorQueryConstraint::OPERATOR_ANCESTOR,
array($select_phids))
->setOrder(ManiphestTaskQuery::ORDER_PRIORITY)
->setViewer($viewer)
->execute();
$tasks = mpull($tasks, null, 'getPHID');
$board_phid = $project->getPHID();
// Regardless of display order, pass tasks to the layout engine in ID order
// so layout is consistent.
$board_tasks = msort($tasks, 'getID');
$layout_engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs(array($board_phid))
->setObjectPHIDs(array_keys($board_tasks))
->setFetchAllBoards(true)
->executeLayout();
$columns = $layout_engine->getColumns($board_phid);
if (!$columns || !$project->getHasWorkboard()) {
$has_normal_columns = false;
foreach ($columns as $column) {
if (!$column->getProxyPHID()) {
$has_normal_columns = true;
break;
}
}
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
if (!$has_normal_columns) {
if (!$can_edit) {
$content = $this->buildNoAccessContent($project);
} else {
$content = $this->buildInitializeContent($project);
}
} else {
if (!$can_edit) {
$content = $this->buildDisabledContent($project);
} else {
$content = $this->buildEnableContent($project);
}
}
if ($content instanceof AphrontResponse) {
return $content;
}
$nav = $this->getProfileMenu();
$nav->selectFilter(PhabricatorProject::ITEM_WORKBOARD);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Workboard'));
return $this->newPage()
->setTitle(
array(
$project->getDisplayName(),
pht('Workboard'),
))
->setNavigation($nav)
->setCrumbs($crumbs)
->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 = $saved->newCopy();
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))
->apply($tasks);
// If this is a batch edit, select the editable tasks in the chosen column
// and ship the user into the batch editor.
$batch_edit = $request->getStr('batch');
if ($batch_edit) {
if ($batch_edit !== self::BATCH_EDIT_ALL) {
$column_id_map = mpull($columns, null, 'getID');
$batch_column = idx($column_id_map, $batch_edit);
if (!$batch_column) {
return new Aphront404Response();
}
$batch_task_phids = $layout_engine->getColumnObjectPHIDs(
$board_phid,
$batch_column->getPHID());
foreach ($batch_task_phids as $key => $batch_task_phid) {
if (empty($task_can_edit_map[$batch_task_phid])) {
unset($batch_task_phids[$key]);
}
}
$batch_tasks = array_select_keys($tasks, $batch_task_phids);
} else {
$batch_tasks = $task_can_edit_map;
}
if (!$batch_tasks) {
$cancel_uri = $this->getURIWithState($board_uri);
return $this->newDialog()
->setTitle(pht('No Editable Tasks'))
->appendParagraph(
pht(
'The selected column contains no visible tasks which you '.
'have permission to edit.'))
->addCancelButton($board_uri);
}
// Create a saved query to hold the working set. This allows us to get
// around URI length limitations with a long "?ids=..." query string.
// For details, see T10268.
$search_engine = id(new ManiphestTaskSearchEngine())
->setViewer($viewer);
$saved_query = $search_engine->newSavedQuery();
$saved_query->setParameter('ids', mpull($batch_tasks, 'getID'));
$search_engine->saveQuery($saved_query);
$query_key = $saved_query->getQueryKey();
$bulk_uri = new PhutilURI("/maniphest/bulk/query/{$query_key}/");
- $bulk_uri->setQueryParam('board', $this->id);
+ $bulk_uri->replaceQueryParam('board', $this->id);
return id(new AphrontRedirectResponse())
->setURI($bulk_uri);
}
$move_id = $request->getStr('move');
if (strlen($move_id)) {
$column_id_map = mpull($columns, null, 'getID');
$move_column = idx($column_id_map, $move_id);
if (!$move_column) {
return new Aphront404Response();
}
$move_task_phids = $layout_engine->getColumnObjectPHIDs(
$board_phid,
$move_column->getPHID());
foreach ($move_task_phids as $key => $move_task_phid) {
if (empty($task_can_edit_map[$move_task_phid])) {
unset($move_task_phids[$key]);
}
}
$move_tasks = array_select_keys($tasks, $move_task_phids);
$cancel_uri = $this->getURIWithState($board_uri);
if (!$move_tasks) {
return $this->newDialog()
->setTitle(pht('No Movable Tasks'))
->appendParagraph(
pht(
'The selected column contains no visible tasks which you '.
'have permission to move.'))
->addCancelButton($cancel_uri);
}
$move_project_phid = $project->getPHID();
$move_column_phid = null;
$move_project = null;
$move_column = null;
$columns = null;
$errors = array();
- if ($request->isFormPost()) {
+ if ($request->isFormOrHiSecPost()) {
$move_project_phid = head($request->getArr('moveProjectPHID'));
if (!$move_project_phid) {
$move_project_phid = $request->getStr('moveProjectPHID');
}
if (!$move_project_phid) {
if ($request->getBool('hasProject')) {
$errors[] = pht('Choose a project to move tasks to.');
}
} else {
$target_project = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withPHIDs(array($move_project_phid))
->executeOne();
if (!$target_project) {
$errors[] = pht('You must choose a valid project.');
} else if (!$project->getHasWorkboard()) {
$errors[] = pht(
'You must choose a project with a workboard.');
} else {
$move_project = $target_project;
}
}
if ($move_project) {
$move_engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs(array($move_project->getPHID()))
->setFetchAllBoards(true)
->executeLayout();
$columns = $move_engine->getColumns($move_project->getPHID());
$columns = mpull($columns, null, 'getPHID');
foreach ($columns as $key => $column) {
if ($column->isHidden()) {
unset($columns[$key]);
}
}
$move_column_phid = $request->getStr('moveColumnPHID');
if (!$move_column_phid) {
if ($request->getBool('hasColumn')) {
$errors[] = pht('Choose a column to move tasks to.');
}
} else {
if (empty($columns[$move_column_phid])) {
$errors[] = pht(
'Choose a valid column on the target workboard to move '.
'tasks to.');
} else if ($columns[$move_column_phid]->getID() == $move_id) {
$errors[] = pht(
'You can not move tasks from a column to itself.');
} else {
$move_column = $columns[$move_column_phid];
}
}
}
}
if ($move_column && $move_project) {
foreach ($move_tasks as $move_task) {
$xactions = array();
// If we're switching projects, get out of the old project first
// and move to the new project.
if ($move_project->getID() != $project->getID()) {
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
->setNewValue(
array(
'-' => array(
$project->getPHID() => $project->getPHID(),
),
'+' => array(
$move_project->getPHID() => $move_project->getPHID(),
),
));
}
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS)
->setNewValue(
array(
array(
'columnPHID' => $move_column->getPHID(),
),
));
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true)
- ->setContentSourceFromRequest($request);
+ ->setContentSourceFromRequest($request)
+ ->setCancelURI($cancel_uri);
$editor->applyTransactions($move_task, $xactions);
}
return id(new AphrontRedirectResponse())
->setURI($cancel_uri);
}
if ($move_project) {
$column_form = id(new AphrontFormView())
->setViewer($viewer)
->appendControl(
id(new AphrontFormSelectControl())
->setName('moveColumnPHID')
->setLabel(pht('Move to Column'))
->setValue($move_column_phid)
->setOptions(mpull($columns, 'getDisplayName', 'getPHID')));
return $this->newDialog()
->setTitle(pht('Move Tasks'))
->setWidth(AphrontDialogView::WIDTH_FORM)
->setErrors($errors)
->addHiddenInput('move', $move_id)
->addHiddenInput('moveProjectPHID', $move_project->getPHID())
->addHiddenInput('hasColumn', true)
->addHiddenInput('hasProject', true)
->appendParagraph(
pht(
'Choose a column on the %s workboard to move tasks to:',
$viewer->renderHandle($move_project->getPHID())))
->appendForm($column_form)
->addSubmitButton(pht('Move Tasks'))
->addCancelButton($cancel_uri);
}
if ($move_project_phid) {
$move_project_phid_value = array($move_project_phid);
} else {
$move_project_phid_value = array();
}
$project_form = id(new AphrontFormView())
->setViewer($viewer)
->appendControl(
id(new AphrontFormTokenizerControl())
->setName('moveProjectPHID')
->setLimit(1)
->setLabel(pht('Move to Project'))
->setValue($move_project_phid_value)
->setDatasource(new PhabricatorProjectDatasource()));
return $this->newDialog()
->setTitle(pht('Move Tasks'))
->setWidth(AphrontDialogView::WIDTH_FORM)
->setErrors($errors)
->addHiddenInput('move', $move_id)
->addHiddenInput('hasProject', true)
->appendForm($project_form)
->addSubmitButton(pht('Continue'))
->addCancelButton($cancel_uri);
}
$board_id = celerity_generate_unique_node_id();
$board = id(new PHUIWorkboardView())
->setUser($viewer)
->setID($board_id)
->addSigil('jx-workboard')
->setMetadata(
array(
'boardPHID' => $project->getPHID(),
));
$visible_columns = array();
$column_phids = array();
$visible_phids = array();
foreach ($columns as $column) {
if (!$this->showHidden) {
if ($column->isHidden()) {
continue;
}
}
$proxy = $column->getProxy();
if ($proxy && !$proxy->isMilestone()) {
// TODO: For now, don't show subproject columns because we can't
// handle tasks with multiple positions yet.
continue;
}
$task_phids = $layout_engine->getColumnObjectPHIDs(
$board_phid,
$column->getPHID());
$column_tasks = array_select_keys($tasks, $task_phids);
-
- // If we aren't using "natural" order, reorder the column by the original
- // query order.
- if ($this->sortKey != PhabricatorProjectColumn::ORDER_NATURAL) {
- $column_tasks = array_select_keys($column_tasks, array_keys($tasks));
- }
-
$column_phid = $column->getPHID();
$visible_columns[$column_phid] = $column;
$column_phids[$column_phid] = $column_tasks;
foreach ($column_tasks as $phid => $task) {
$visible_phids[$phid] = $phid;
}
}
$rendering_engine = id(new PhabricatorBoardRenderingEngine())
->setViewer($viewer)
->setObjects(array_select_keys($tasks, $visible_phids))
->setEditMap($task_can_edit_map)
->setExcludedProjectPHIDs($select_phids);
$templates = array();
- $column_maps = array();
$all_tasks = array();
+ $column_templates = array();
+ $sounds = array();
foreach ($visible_columns as $column_phid => $column) {
$column_tasks = $column_phids[$column_phid];
$panel = id(new PHUIWorkpanelView())
->setHeader($column->getDisplayName())
->setSubHeader($column->getDisplayType())
->addSigil('workpanel');
$proxy = $column->getProxy();
if ($proxy) {
$proxy_id = $proxy->getID();
$href = $this->getApplicationURI("view/{$proxy_id}/");
$panel->setHref($href);
}
$header_icon = $column->getHeaderIcon();
if ($header_icon) {
$panel->setHeaderIcon($header_icon);
}
$display_class = $column->getDisplayClass();
if ($display_class) {
$panel->addClass($display_class);
}
if ($column->isHidden()) {
$panel->addClass('project-panel-hidden');
}
$column_menu = $this->buildColumnMenu($project, $column);
$panel->addHeaderAction($column_menu);
+ if ($column->canHaveTrigger()) {
+ $trigger_menu = $this->buildTriggerMenu($column);
+ $panel->addHeaderAction($trigger_menu);
+ }
+
$count_tag = id(new PHUITagView())
->setType(PHUITagView::TYPE_SHADE)
->setColor(PHUITagView::COLOR_BLUE)
->addSigil('column-points')
->setName(
javelin_tag(
'span',
array(
'sigil' => 'column-points-content',
),
pht('-')))
->setStyle('display: none');
$panel->setHeaderTag($count_tag);
$cards = id(new PHUIObjectItemListView())
->setUser($viewer)
->setFlush(true)
->setAllowEmptyList(true)
->addSigil('project-column')
->setItemClass('phui-workcard')
->setMetadata(
array(
'columnPHID' => $column->getPHID(),
'pointLimit' => $column->getPointLimit(),
));
+ $card_phids = array();
foreach ($column_tasks as $task) {
$object_phid = $task->getPHID();
$card = $rendering_engine->renderCard($object_phid);
$templates[$object_phid] = hsprintf('%s', $card->getItem());
- $column_maps[$column_phid][] = $object_phid;
+ $card_phids[] = $object_phid;
$all_tasks[$object_phid] = $task;
}
$panel->setCards($cards);
$board->addPanel($panel);
+
+ $drop_effects = $column->getDropEffects();
+ $drop_effects = mpull($drop_effects, 'toDictionary');
+
+ $preview_effect = null;
+ if ($column->canHaveTrigger()) {
+ $trigger = $column->getTrigger();
+ if ($trigger) {
+ $preview_effect = $trigger->getPreviewEffect()
+ ->toDictionary();
+
+ foreach ($trigger->getSoundEffects() as $sound) {
+ $sounds[] = $sound;
+ }
+ }
+ }
+
+ $column_templates[] = array(
+ 'columnPHID' => $column_phid,
+ 'effects' => $drop_effects,
+ 'cardPHIDs' => $card_phids,
+ 'triggerPreviewEffect' => $preview_effect,
+ );
+ }
+
+ $order_key = $this->sortKey;
+
+ $ordering_map = PhabricatorProjectColumnOrder::getEnabledOrders();
+ $ordering = id(clone $ordering_map[$order_key])
+ ->setViewer($viewer);
+
+ $headers = $ordering->getHeadersForObjects($all_tasks);
+ $headers = mpull($headers, 'toDictionary');
+
+ $vectors = $ordering->getSortVectorsForObjects($all_tasks);
+ $vector_map = array();
+ foreach ($vectors as $task_phid => $vector) {
+ $vector_map[$task_phid][$order_key] = $vector;
+ }
+
+ $header_keys = $ordering->getHeaderKeysForObjects($all_tasks);
+
+ $order_maps = array();
+ $order_maps[] = $ordering->toDictionary();
+
+ $properties = array();
+ foreach ($all_tasks as $task) {
+ $properties[$task->getPHID()] =
+ PhabricatorBoardResponseEngine::newTaskProperties($task);
}
$behavior_config = array(
'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'),
'uploadURI' => '/file/dropupload/',
'coverURI' => $this->getApplicationURI('cover/'),
'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(),
'pointsEnabled' => ManiphestTaskPoints::getIsEnabled(),
'boardPHID' => $project->getPHID(),
'order' => $this->sortKey,
+ 'orders' => $order_maps,
+ 'headers' => $headers,
+ 'headerKeys' => $header_keys,
'templateMap' => $templates,
- 'columnMaps' => $column_maps,
- 'orderMaps' => mpull($all_tasks, 'getWorkboardOrderVectors'),
- 'propertyMaps' => mpull($all_tasks, 'getWorkboardProperties'),
+ 'orderMaps' => $vector_map,
+ 'propertyMaps' => $properties,
+ 'columnTemplates' => $column_templates,
'boardID' => $board_id,
'projectPHID' => $project->getPHID(),
+ 'preloadSounds' => $sounds,
);
$this->initBehavior('project-boards', $behavior_config);
-
$sort_menu = $this->buildSortMenu(
$viewer,
$project,
- $this->sortKey);
+ $this->sortKey,
+ $ordering_map);
$filter_menu = $this->buildFilterMenu(
$viewer,
$project,
$custom_query,
$search_engine,
$query_key);
$manage_menu = $this->buildManageMenu($project, $this->showHidden);
$header_link = phutil_tag(
'a',
array(
'href' => $this->getApplicationURI('profile/'.$project->getID().'/'),
),
$project->getName());
$board_box = id(new PHUIBoxView())
->appendChild($board)
->addClass('project-board-wrapper');
$nav = $this->getProfileMenu();
$divider = id(new PHUIListItemView())
->setType(PHUIListItemView::TYPE_DIVIDER);
$fullscreen = $this->buildFullscreenMenu();
- $crumbs = $this->buildApplicationCrumbs();
+ $crumbs = $this->newWorkboardCrumbs();
$crumbs->addTextCrumb(pht('Workboard'));
$crumbs->setBorder(true);
$crumbs->addAction($sort_menu);
$crumbs->addAction($filter_menu);
$crumbs->addAction($divider);
$crumbs->addAction($manage_menu);
$crumbs->addAction($fullscreen);
$page = $this->newPage()
->setTitle(
array(
$project->getDisplayName(),
pht('Workboard'),
))
->setPageObjectPHIDs(array($project->getPHID()))
->setShowFooter(false)
->setNavigation($nav)
->setCrumbs($crumbs)
->addQuicksandConfig(
array(
'boardConfig' => $behavior_config,
))
->appendChild(
array(
$board_box,
));
$background = $project->getDisplayWorkboardBackgroundColor();
require_celerity_resource('phui-workboard-color-css');
if ($background !== null) {
$background_color_class = "phui-workboard-{$background}";
$page->addClass('phui-workboard-color');
$page->addClass($background_color_class);
} else {
$page->addClass('phui-workboard-no-color');
}
return $page;
}
private function readRequestState() {
$request = $this->getRequest();
$project = $this->getProject();
$this->showHidden = $request->getBool('hidden');
$this->id = $project->getID();
$sort_key = $this->getDefaultSort($project);
$request_sort = $request->getStr('order');
if ($this->isValidSort($request_sort)) {
$sort_key = $request_sort;
}
$this->sortKey = $sort_key;
}
private function getDefaultSort(PhabricatorProject $project) {
$default_sort = $project->getDefaultWorkboardSort();
if ($this->isValidSort($default_sort)) {
return $default_sort;
}
- return PhabricatorProjectColumn::DEFAULT_ORDER;
+ return PhabricatorProjectColumnNaturalOrder::ORDERKEY;
}
private function getDefaultFilter(PhabricatorProject $project) {
$default_filter = $project->getDefaultWorkboardFilter();
if (strlen($default_filter)) {
return $default_filter;
}
return 'open';
}
private function isValidSort($sort) {
- switch ($sort) {
- case PhabricatorProjectColumn::ORDER_NATURAL:
- case PhabricatorProjectColumn::ORDER_PRIORITY:
- return true;
- }
-
- return false;
+ $map = PhabricatorProjectColumnOrder::getEnabledOrders();
+ return isset($map[$sort]);
}
private function buildSortMenu(
PhabricatorUser $viewer,
PhabricatorProject $project,
- $sort_key) {
-
- $sort_icon = id(new PHUIIconView())
- ->setIcon('fa-sort-amount-asc bluegrey');
-
- $named = array(
- PhabricatorProjectColumn::ORDER_NATURAL => pht('Natural'),
- PhabricatorProjectColumn::ORDER_PRIORITY => pht('Sort by Priority'),
- );
+ $sort_key,
+ array $ordering_map) {
$base_uri = $this->getURIWithState();
$items = array();
- foreach ($named as $key => $name) {
- $is_selected = ($key == $sort_key);
+ foreach ($ordering_map as $key => $ordering) {
+ // TODO: It would be desirable to build a real "PHUIIconView" here, but
+ // the pathway for threading that through all the view classes ends up
+ // being fairly complex, since some callers read the icon out of other
+ // views. For now, just stick with a string.
+ $ordering_icon = $ordering->getMenuIconIcon();
+ $ordering_name = $ordering->getDisplayName();
+
+ $is_selected = ($key === $sort_key);
if ($is_selected) {
- $active_order = $name;
+ $active_name = $ordering_name;
+ $active_icon = $ordering_icon;
}
$item = id(new PhabricatorActionView())
- ->setIcon('fa-sort-amount-asc')
+ ->setIcon($ordering_icon)
->setSelected($is_selected)
- ->setName($name);
+ ->setName($ordering_name);
$uri = $base_uri->alter('order', $key);
$item->setHref($uri);
$items[] = $item;
}
$id = $project->getID();
$save_uri = "default/{$id}/sort/";
$save_uri = $this->getApplicationURI($save_uri);
$save_uri = $this->getURIWithState($save_uri, $force = true);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
+ $items[] = id(new PhabricatorActionView())
+ ->setType(PhabricatorActionView::TYPE_DIVIDER);
+
$items[] = id(new PhabricatorActionView())
->setIcon('fa-floppy-o')
->setName(pht('Save as Default'))
->setHref($save_uri)
->setWorkflow(true)
->setDisabled(!$can_edit);
$sort_menu = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($items as $item) {
$sort_menu->addAction($item);
}
$sort_button = id(new PHUIListItemView())
- ->setName($active_order)
- ->setIcon('fa-sort-amount-asc')
+ ->setName($active_name)
+ ->setIcon($active_icon)
->setHref('#')
->addSigil('boards-dropdown-menu')
->setMetadata(
array(
'items' => hsprintf('%s', $sort_menu),
));
return $sort_button;
}
private function buildFilterMenu(
PhabricatorUser $viewer,
PhabricatorProject $project,
$custom_query,
PhabricatorApplicationSearchEngine $engine,
$query_key) {
$named = array(
'open' => pht('Open Tasks'),
'all' => pht('All Tasks'),
);
if ($viewer->isLoggedIn()) {
$named['assigned'] = pht('Assigned to Me');
}
if ($custom_query) {
$named[$custom_query->getQueryKey()] = pht('Custom Filter');
}
$items = array();
foreach ($named as $key => $name) {
$is_selected = ($key == $query_key);
if ($is_selected) {
$active_filter = $name;
}
$is_custom = false;
if ($custom_query) {
$is_custom = ($key == $custom_query->getQueryKey());
}
$item = id(new PhabricatorActionView())
->setIcon('fa-search')
->setSelected($is_selected)
->setName($name);
if ($is_custom) {
$uri = $this->getApplicationURI(
'board/'.$this->id.'/filter/query/'.$key.'/');
$item->setWorkflow(true);
} else {
$uri = $engine->getQueryResultsPageURI($key);
}
$uri = $this->getURIWithState($uri)
- ->setQueryParam('filter', null);
+ ->removeQueryParam('filter');
$item->setHref($uri);
$items[] = $item;
}
$id = $project->getID();
$filter_uri = $this->getApplicationURI("board/{$id}/filter/");
$filter_uri = $this->getURIWithState($filter_uri, $force = true);
$items[] = id(new PhabricatorActionView())
->setIcon('fa-cog')
->setHref($filter_uri)
->setWorkflow(true)
->setName(pht('Advanced Filter...'));
$save_uri = "default/{$id}/filter/";
$save_uri = $this->getApplicationURI($save_uri);
$save_uri = $this->getURIWithState($save_uri, $force = true);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
+ $items[] = id(new PhabricatorActionView())
+ ->setType(PhabricatorActionView::TYPE_DIVIDER);
+
$items[] = id(new PhabricatorActionView())
->setIcon('fa-floppy-o')
->setName(pht('Save as Default'))
->setHref($save_uri)
->setWorkflow(true)
->setDisabled(!$can_edit);
$filter_menu = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($items as $item) {
$filter_menu->addAction($item);
}
$filter_button = id(new PHUIListItemView())
->setName($active_filter)
->setIcon('fa-search')
->setHref('#')
->addSigil('boards-dropdown-menu')
->setMetadata(
array(
'items' => hsprintf('%s', $filter_menu),
));
return $filter_button;
}
private function buildManageMenu(
PhabricatorProject $project,
$show_hidden) {
$request = $this->getRequest();
$viewer = $request->getUser();
$id = $project->getID();
$manage_uri = $this->getApplicationURI("board/{$id}/manage/");
$add_uri = $this->getApplicationURI("board/{$id}/edit/");
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
$manage_items = array();
$manage_items[] = id(new PhabricatorActionView())
->setIcon('fa-plus')
->setName(pht('Add Column'))
->setHref($add_uri)
->setDisabled(!$can_edit)
->setWorkflow(true);
$reorder_uri = $this->getApplicationURI("board/{$id}/reorder/");
$manage_items[] = id(new PhabricatorActionView())
->setIcon('fa-exchange')
->setName(pht('Reorder Columns'))
->setHref($reorder_uri)
->setDisabled(!$can_edit)
->setWorkflow(true);
if ($show_hidden) {
$hidden_uri = $this->getURIWithState()
- ->setQueryParam('hidden', null);
+ ->removeQueryParam('hidden');
$hidden_icon = 'fa-eye-slash';
$hidden_text = pht('Hide Hidden Columns');
} else {
$hidden_uri = $this->getURIWithState()
- ->setQueryParam('hidden', 'true');
+ ->replaceQueryParam('hidden', 'true');
$hidden_icon = 'fa-eye';
$hidden_text = pht('Show Hidden Columns');
}
$manage_items[] = id(new PhabricatorActionView())
->setIcon($hidden_icon)
->setName($hidden_text)
->setHref($hidden_uri);
$manage_items[] = id(new PhabricatorActionView())
->setType(PhabricatorActionView::TYPE_DIVIDER);
$background_uri = $this->getApplicationURI("board/{$id}/background/");
$manage_items[] = id(new PhabricatorActionView())
->setIcon('fa-paint-brush')
->setName(pht('Change Background Color'))
->setHref($background_uri)
->setDisabled(!$can_edit)
->setWorkflow(false);
$manage_uri = $this->getApplicationURI("board/{$id}/manage/");
$manage_items[] = id(new PhabricatorActionView())
->setIcon('fa-gear')
->setName(pht('Manage Workboard'))
->setHref($manage_uri);
$batch_edit_uri = $request->getRequestURI();
- $batch_edit_uri->setQueryParam('batch', self::BATCH_EDIT_ALL);
+ $batch_edit_uri->replaceQueryParam('batch', self::BATCH_EDIT_ALL);
$can_batch_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
PhabricatorApplication::getByClass('PhabricatorManiphestApplication'),
ManiphestBulkEditCapability::CAPABILITY);
$manage_menu = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($manage_items as $item) {
$manage_menu->addAction($item);
}
$manage_button = id(new PHUIListItemView())
->setIcon('fa-cog')
->setHref('#')
->addSigil('boards-dropdown-menu')
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => pht('Manage'),
'align' => 'S',
'items' => hsprintf('%s', $manage_menu),
));
return $manage_button;
}
private function buildFullscreenMenu() {
$up = id(new PHUIListItemView())
->setIcon('fa-arrows-alt')
->setHref('#')
->addClass('phui-workboard-expand-icon')
->addSigil('jx-toggle-class')
->addSigil('has-tooltip')
->setMetaData(array(
'tip' => pht('Fullscreen'),
'map' => array(
'phabricator-standard-page' => 'phui-workboard-fullscreen',
),
));
return $up;
}
private function buildColumnMenu(
PhabricatorProject $project,
PhabricatorProjectColumn $column) {
$request = $this->getRequest();
$viewer = $request->getUser();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
$column_items = array();
if ($column->getProxyPHID()) {
$default_phid = $column->getProxyPHID();
} else {
$default_phid = $column->getProjectPHID();
}
$specs = id(new ManiphestEditEngine())
->setViewer($viewer)
->newCreateActionSpecifications(array());
foreach ($specs as $spec) {
$column_items[] = id(new PhabricatorActionView())
->setIcon($spec['icon'])
->setName($spec['name'])
->setHref($spec['uri'])
->setDisabled($spec['disabled'])
->addSigil('column-add-task')
->setMetadata(
array(
'createURI' => $spec['uri'],
'columnPHID' => $column->getPHID(),
'boardPHID' => $project->getPHID(),
'projectPHID' => $default_phid,
));
}
- if (count($specs) > 1) {
- $column_items[] = id(new PhabricatorActionView())
- ->setType(PhabricatorActionView::TYPE_DIVIDER);
- }
+ $column_items[] = id(new PhabricatorActionView())
+ ->setType(PhabricatorActionView::TYPE_DIVIDER);
$batch_edit_uri = $request->getRequestURI();
- $batch_edit_uri->setQueryParam('batch', $column->getID());
+ $batch_edit_uri->replaceQueryParam('batch', $column->getID());
$can_batch_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
PhabricatorApplication::getByClass('PhabricatorManiphestApplication'),
ManiphestBulkEditCapability::CAPABILITY);
$column_items[] = id(new PhabricatorActionView())
->setIcon('fa-list-ul')
->setName(pht('Bulk Edit Tasks...'))
->setHref($batch_edit_uri)
->setDisabled(!$can_batch_edit);
$batch_move_uri = $request->getRequestURI();
- $batch_move_uri->setQueryParam('move', $column->getID());
+ $batch_move_uri->replaceQueryParam('move', $column->getID());
$column_items[] = id(new PhabricatorActionView())
->setIcon('fa-arrow-right')
->setName(pht('Move Tasks to Column...'))
->setHref($batch_move_uri)
->setWorkflow(true);
$query_uri = $request->getRequestURI();
- $query_uri->setQueryParam('queryColumnID', $column->getID());
+ $query_uri->replaceQueryParam('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'))
->setIcon('fa-pencil')
->setHref($this->getApplicationURI($edit_uri))
->setDisabled(!$can_edit)
->setWorkflow(true);
$can_hide = ($can_edit && !$column->isDefaultColumn());
$hide_uri = 'board/'.$this->id.'/hide/'.$column->getID().'/';
$hide_uri = $this->getApplicationURI($hide_uri);
$hide_uri = $this->getURIWithState($hide_uri);
if (!$column->isHidden()) {
$column_items[] = id(new PhabricatorActionView())
->setName(pht('Hide Column'))
->setIcon('fa-eye-slash')
->setHref($hide_uri)
->setDisabled(!$can_hide)
->setWorkflow(true);
} else {
$column_items[] = id(new PhabricatorActionView())
->setName(pht('Show Column'))
->setIcon('fa-eye')
->setHref($hide_uri)
->setDisabled(!$can_hide)
->setWorkflow(true);
}
$column_menu = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($column_items as $item) {
$column_menu->addAction($item);
}
$column_button = id(new PHUIIconView())
- ->setIcon('fa-caret-down')
+ ->setIcon('fa-pencil')
->setHref('#')
->addSigil('boards-dropdown-menu')
->setMetadata(
array(
'items' => hsprintf('%s', $column_menu),
));
return $column_button;
}
+ private function buildTriggerMenu(PhabricatorProjectColumn $column) {
+ $viewer = $this->getViewer();
+ $trigger = $column->getTrigger();
+
+ $can_edit = PhabricatorPolicyFilter::hasCapability(
+ $viewer,
+ $column,
+ PhabricatorPolicyCapability::CAN_EDIT);
+
+ $trigger_items = array();
+ if (!$trigger) {
+ $set_uri = $this->getApplicationURI(
+ new PhutilURI(
+ 'trigger/edit/',
+ array(
+ 'columnPHID' => $column->getPHID(),
+ )));
+
+ $trigger_items[] = id(new PhabricatorActionView())
+ ->setIcon('fa-cogs')
+ ->setName(pht('New Trigger...'))
+ ->setHref($set_uri)
+ ->setDisabled(!$can_edit);
+ } else {
+ $trigger_items[] = id(new PhabricatorActionView())
+ ->setIcon('fa-cogs')
+ ->setName(pht('View Trigger'))
+ ->setHref($trigger->getURI())
+ ->setDisabled(!$can_edit);
+ }
+
+ $remove_uri = $this->getApplicationURI(
+ new PhutilURI(
+ urisprintf(
+ 'column/remove/%d/',
+ $column->getID())));
+
+ $trigger_items[] = id(new PhabricatorActionView())
+ ->setIcon('fa-times')
+ ->setName(pht('Remove Trigger'))
+ ->setHref($remove_uri)
+ ->setWorkflow(true)
+ ->setDisabled(!$can_edit || !$trigger);
+
+ $trigger_menu = id(new PhabricatorActionListView())
+ ->setUser($viewer);
+ foreach ($trigger_items as $item) {
+ $trigger_menu->addAction($item);
+ }
+
+ if ($trigger) {
+ $trigger_icon = 'fa-cogs';
+ } else {
+ $trigger_icon = 'fa-cogs grey';
+ }
+
+ $trigger_button = id(new PHUIIconView())
+ ->setIcon($trigger_icon)
+ ->setHref('#')
+ ->addSigil('boards-dropdown-menu')
+ ->addSigil('trigger-preview')
+ ->setMetadata(
+ array(
+ 'items' => hsprintf('%s', $trigger_menu),
+ 'columnPHID' => $column->getPHID(),
+ ));
+
+ return $trigger_button;
+ }
/**
* Add current state parameters (like order and the visibility of hidden
* columns) to a URI.
*
* This allows actions which toggle or adjust one piece of state to keep
* the rest of the board state persistent. If no URI is provided, this method
* starts with the request URI.
*
* @param string|null URI to add state parameters to.
* @param bool True to explicitly include all state.
* @return PhutilURI URI with state parameters.
*/
private function getURIWithState($base = null, $force = false) {
$project = $this->getProject();
if ($base === null) {
- $base = $this->getRequest()->getRequestURI();
+ $base = $this->getRequest()->getPath();
}
$base = new PhutilURI($base);
if ($force || ($this->sortKey != $this->getDefaultSort($project))) {
- $base->setQueryParam('order', $this->sortKey);
+ if ($this->sortKey !== null) {
+ $base->replaceQueryParam('order', $this->sortKey);
+ } else {
+ $base->removeQueryParam('order');
+ }
} else {
- $base->setQueryParam('order', null);
+ $base->removeQueryParam('order');
}
if ($force || ($this->queryKey != $this->getDefaultFilter($project))) {
- $base->setQueryParam('filter', $this->queryKey);
+ if ($this->queryKey !== null) {
+ $base->replaceQueryParam('filter', $this->queryKey);
+ } else {
+ $base->removeQueryParam('filter');
+ }
} else {
- $base->setQueryParam('filter', null);
+ $base->removeQueryParam('filter');
}
- $base->setQueryParam('hidden', $this->showHidden ? 'true' : null);
+ if ($this->showHidden) {
+ $base->replaceQueryParam('hidden', 'true');
+ } else {
+ $base->removeQueryParam('hidden');
+ }
return $base;
}
private function buildInitializeContent(PhabricatorProject $project) {
$request = $this->getRequest();
$viewer = $this->getViewer();
$type = $request->getStr('initialize-type');
$id = $project->getID();
$profile_uri = $this->getApplicationURI("profile/{$id}/");
$board_uri = $this->getApplicationURI("board/{$id}/");
$import_uri = $this->getApplicationURI("board/{$id}/import/");
$set_default = $request->getBool('default');
if ($set_default) {
$this
->getProfileMenuEngine()
->adjustDefault(PhabricatorProject::ITEM_WORKBOARD);
}
if ($request->isFormPost()) {
if ($type == 'backlog-only') {
$column = PhabricatorProjectColumn::initializeNewColumn($viewer)
->setSequence(0)
->setProperty('isDefault', true)
->setProjectPHID($project->getPHID())
->save();
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(
PhabricatorProjectWorkboardTransaction::TRANSACTIONTYPE)
->setNewValue(1);
id(new PhabricatorProjectTransactionEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($project, $xactions);
return id(new AphrontRedirectResponse())
->setURI($board_uri);
} else {
return id(new AphrontRedirectResponse())
->setURI($import_uri);
}
}
// TODO: Tailor this UI if the project is already a parent project. We
// should not offer options for creating a parent project workboard, since
// they can't have their own columns.
$new_selector = id(new AphrontFormRadioButtonControl())
->setLabel(pht('Columns'))
->setName('initialize-type')
->setValue('backlog-only')
->addButton(
'backlog-only',
pht('New Empty Board'),
pht('Create a new board with just a backlog column.'))
->addButton(
'import',
pht('Import Columns'),
pht('Import board columns from another project.'));
$default_checkbox = id(new AphrontFormCheckboxControl())
->setLabel(pht('Make Default'))
->addCheckbox(
'default',
1,
pht('Make the workboard the default view for this project.'),
true);
$form = id(new AphrontFormView())
->setUser($viewer)
->addHiddenInput('initialize', 1)
->appendRemarkupInstructions(
pht('The workboard for this project has not been created yet.'))
->appendControl($new_selector)
->appendControl($default_checkbox)
->appendControl(
id(new AphrontFormSubmitControl())
->addCancelButton($profile_uri)
->setValue(pht('Create Workboard')));
$box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Create Workboard'))
->setForm($form);
return $box;
}
private function buildNoAccessContent(PhabricatorProject $project) {
$viewer = $this->getViewer();
$id = $project->getID();
$profile_uri = $this->getApplicationURI("profile/{$id}/");
return $this->newDialog()
->setTitle(pht('Unable to Create Workboard'))
->appendParagraph(
pht(
'The workboard for this project has not been created yet, '.
'but you do not have permission to create it. Only users '.
'who can edit this project can create a workboard for it.'))
->addCancelButton($profile_uri);
}
private function buildEnableContent(PhabricatorProject $project) {
$request = $this->getRequest();
$viewer = $this->getViewer();
$id = $project->getID();
$profile_uri = $this->getApplicationURI("profile/{$id}/");
$board_uri = $this->getApplicationURI("board/{$id}/");
if ($request->isFormPost()) {
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(
PhabricatorProjectWorkboardTransaction::TRANSACTIONTYPE)
->setNewValue(1);
id(new PhabricatorProjectTransactionEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($project, $xactions);
return id(new AphrontRedirectResponse())
->setURI($board_uri);
}
return $this->newDialog()
->setTitle(pht('Workboard Disabled'))
->addHiddenInput('initialize', 1)
->appendParagraph(
pht(
'This workboard has been disabled, but can be restored to its '.
'former glory.'))
->addCancelButton($profile_uri)
->addSubmitButton(pht('Enable Workboard'));
}
private function buildDisabledContent(PhabricatorProject $project) {
$viewer = $this->getViewer();
$id = $project->getID();
$profile_uri = $this->getApplicationURI("profile/{$id}/");
return $this->newDialog()
->setTitle(pht('Workboard Disabled'))
->appendParagraph(
pht(
'This workboard has been disabled, and you do not have permission '.
'to enable it. Only users who can edit this project can restore '.
'the workboard.'))
->addCancelButton($profile_uri);
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectColumnDetailController.php b/src/applications/project/controller/PhabricatorProjectColumnDetailController.php
index 24efec5eb..781461a81 100644
--- a/src/applications/project/controller/PhabricatorProjectColumnDetailController.php
+++ b/src/applications/project/controller/PhabricatorProjectColumnDetailController.php
@@ -1,111 +1,111 @@
<?php
final class PhabricatorProjectColumnDetailController
extends PhabricatorProjectBoardController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$project_id = $request->getURIData('projectID');
$project = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
))
->withIDs(array($project_id))
->needImages(true)
->executeOne();
if (!$project) {
return new Aphront404Response();
}
$this->setProject($project);
$project_id = $project->getID();
$column = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
))
->executeOne();
if (!$column) {
return new Aphront404Response();
}
$timeline = $this->buildTransactionTimeline(
$column,
new PhabricatorProjectColumnTransactionQuery());
$timeline->setShouldTerminate(true);
$title = $column->getDisplayName();
$header = $this->buildHeaderView($column);
$properties = $this->buildPropertyView($column);
$crumbs = $this->buildApplicationCrumbs();
- $crumbs->addTextCrumb(pht('Workboard'), "/project/board/{$project_id}/");
+ $crumbs->addTextCrumb(pht('Workboard'), $project->getWorkboardURI());
$crumbs->addTextCrumb(pht('Column: %s', $title));
$crumbs->setBorder(true);
$nav = $this->getProfileMenu();
require_celerity_resource('project-view-css');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->addClass('project-view-home')
->addClass('project-view-people-home')
->setMainColumn(array(
$properties,
$timeline,
));
return $this->newPage()
->setTitle($title)
->setNavigation($nav)
->setCrumbs($crumbs)
->appendChild($view);
}
private function buildHeaderView(PhabricatorProjectColumn $column) {
$viewer = $this->getViewer();
$header = id(new PHUIHeaderView())
->setHeader(pht('Column: %s', $column->getDisplayName()))
->setUser($viewer);
if ($column->isHidden()) {
$header->setStatus('fa-ban', 'dark', pht('Hidden'));
}
return $header;
}
private function buildPropertyView(
PhabricatorProjectColumn $column) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($column);
$limit = $column->getPointLimit();
if ($limit === null) {
$limit_text = pht('No Limit');
} else {
$limit_text = $limit;
}
$properties->addProperty(pht('Point Limit'), $limit_text);
$box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Details'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($properties);
return $box;
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectColumnEditController.php b/src/applications/project/controller/PhabricatorProjectColumnEditController.php
index 94277c92e..9ddb2b7d8 100644
--- a/src/applications/project/controller/PhabricatorProjectColumnEditController.php
+++ b/src/applications/project/controller/PhabricatorProjectColumnEditController.php
@@ -1,145 +1,143 @@
<?php
final class PhabricatorProjectColumnEditController
extends PhabricatorProjectBoardController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$project_id = $request->getURIData('projectID');
$project = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($project_id))
->needImages(true)
->executeOne();
if (!$project) {
return new Aphront404Response();
}
$this->setProject($project);
$is_new = ($id ? false : true);
if (!$is_new) {
$column = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$column) {
return new Aphront404Response();
}
} else {
$column = PhabricatorProjectColumn::initializeNewColumn($viewer);
}
$e_name = null;
$e_limit = null;
$v_limit = $column->getPointLimit();
$v_name = $column->getName();
$validation_exception = null;
- $base_uri = '/board/'.$project_id.'/';
- $view_uri = $this->getApplicationURI($base_uri);
+ $view_uri = $project->getWorkboardURI();
if ($request->isFormPost()) {
$v_name = $request->getStr('name');
$v_limit = $request->getStr('limit');
if ($is_new) {
$column->setProjectPHID($project->getPHID());
$column->attachProject($project);
$columns = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withProjectPHIDs(array($project->getPHID()))
->execute();
$new_sequence = 1;
if ($columns) {
$values = mpull($columns, 'getSequence');
$new_sequence = max($values) + 1;
}
$column->setSequence($new_sequence);
}
$xactions = array();
- $type_name = PhabricatorProjectColumnTransaction::TYPE_NAME;
- $type_limit = PhabricatorProjectColumnTransaction::TYPE_LIMIT;
+ $type_name = PhabricatorProjectColumnNameTransaction::TRANSACTIONTYPE;
+ $type_limit = PhabricatorProjectColumnLimitTransaction::TRANSACTIONTYPE;
if (!$column->getProxy()) {
$xactions[] = id(new PhabricatorProjectColumnTransaction())
->setTransactionType($type_name)
->setNewValue($v_name);
}
$xactions[] = id(new PhabricatorProjectColumnTransaction())
->setTransactionType($type_limit)
->setNewValue($v_limit);
try {
$editor = id(new PhabricatorProjectColumnTransactionEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
- ->setContinueOnMissingFields(true)
->setContentSourceFromRequest($request)
->applyTransactions($column, $xactions);
return id(new AphrontRedirectResponse())->setURI($view_uri);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$e_name = $ex->getShortMessage($type_name);
$e_limit = $ex->getShortMessage($type_limit);
$validation_exception = $ex;
}
}
$form = id(new AphrontFormView())
->setUser($request->getUser());
if (!$column->getProxy()) {
$form->appendChild(
id(new AphrontFormTextControl())
->setValue($v_name)
->setLabel(pht('Name'))
->setName('name')
->setError($e_name));
}
$form->appendChild(
id(new AphrontFormTextControl())
->setValue($v_limit)
->setLabel(pht('Point Limit'))
->setName('limit')
->setError($e_limit)
->setCaption(
pht('Maximum number of points of tasks allowed in the column.')));
if ($is_new) {
$title = pht('Create Column');
$submit = pht('Create Column');
} else {
$title = pht('Edit %s', $column->getDisplayName());
$submit = pht('Save Column');
}
return $this->newDialog()
->setWidth(AphrontDialogView::WIDTH_FORM)
->setTitle($title)
->appendForm($form)
->setValidationException($validation_exception)
->addCancelButton($view_uri)
->addSubmitButton($submit);
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectColumnHideController.php b/src/applications/project/controller/PhabricatorProjectColumnHideController.php
index 1dd5e47ec..254beab78 100644
--- a/src/applications/project/controller/PhabricatorProjectColumnHideController.php
+++ b/src/applications/project/controller/PhabricatorProjectColumnHideController.php
@@ -1,147 +1,149 @@
<?php
final class PhabricatorProjectColumnHideController
extends PhabricatorProjectBoardController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$project_id = $request->getURIData('projectID');
$project = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($project_id))
->executeOne();
if (!$project) {
return new Aphront404Response();
}
$this->setProject($project);
$column = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$column) {
return new Aphront404Response();
}
$column_phid = $column->getPHID();
- $view_uri = $this->getApplicationURI('/board/'.$project_id.'/');
+ $view_uri = $project->getWorkboardURI();
$view_uri = new PhutilURI($view_uri);
foreach ($request->getPassthroughRequestData() as $key => $value) {
- $view_uri->setQueryParam($key, $value);
+ $view_uri->replaceQueryParam($key, $value);
}
if ($column->isDefaultColumn()) {
return $this->newDialog()
->setTitle(pht('Can Not Hide Default Column'))
->appendParagraph(
pht('You can not hide the default/backlog column on a board.'))
->addCancelButton($view_uri, pht('Okay'));
}
$proxy = $column->getProxy();
if ($request->isFormPost()) {
if ($proxy) {
if ($proxy->isArchived()) {
$new_status = PhabricatorProjectStatus::STATUS_ACTIVE;
} else {
$new_status = PhabricatorProjectStatus::STATUS_ARCHIVED;
}
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(
PhabricatorProjectStatusTransaction::TRANSACTIONTYPE)
->setNewValue($new_status);
id(new PhabricatorProjectTransactionEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($proxy, $xactions);
} else {
if ($column->isHidden()) {
$new_status = PhabricatorProjectColumn::STATUS_ACTIVE;
} else {
$new_status = PhabricatorProjectColumn::STATUS_HIDDEN;
}
- $type_status = PhabricatorProjectColumnTransaction::TYPE_STATUS;
+ $type_status =
+ PhabricatorProjectColumnStatusTransaction::TRANSACTIONTYPE;
+
$xactions = array(
id(new PhabricatorProjectColumnTransaction())
->setTransactionType($type_status)
->setNewValue($new_status),
);
$editor = id(new PhabricatorProjectColumnTransactionEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setContentSourceFromRequest($request)
->applyTransactions($column, $xactions);
}
return id(new AphrontRedirectResponse())->setURI($view_uri);
}
if ($proxy) {
if ($column->isHidden()) {
$title = pht('Activate and Show Column');
$body = pht(
'This column is hidden because it represents an archived '.
'subproject. Do you want to activate the subproject so the '.
'column is visible again?');
$button = pht('Activate Subproject');
} else {
$title = pht('Archive and Hide Column');
$body = pht(
'This column is visible because it represents an active '.
'subproject. Do you want to hide the column by archiving the '.
'subproject?');
$button = pht('Archive Subproject');
}
} else {
if ($column->isHidden()) {
$title = pht('Show Column');
$body = pht('Are you sure you want to show this column?');
$button = pht('Show Column');
} else {
$title = pht('Hide Column');
$body = pht(
'Are you sure you want to hide this column? It will no longer '.
'appear on the workboard.');
$button = pht('Hide Column');
}
}
$dialog = $this->newDialog()
->setWidth(AphrontDialogView::WIDTH_FORM)
->setTitle($title)
->appendChild($body)
->setDisableWorkflowOnCancel(true)
->addCancelButton($view_uri)
->addSubmitButton($button);
foreach ($request->getPassthroughRequestData() as $key => $value) {
$dialog->addHiddenInput($key, $value);
}
return $dialog;
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectColumnRemoveTriggerController.php b/src/applications/project/controller/PhabricatorProjectColumnRemoveTriggerController.php
new file mode 100644
index 000000000..9bb92e5a3
--- /dev/null
+++ b/src/applications/project/controller/PhabricatorProjectColumnRemoveTriggerController.php
@@ -0,0 +1,60 @@
+<?php
+
+final class PhabricatorProjectColumnRemoveTriggerController
+ extends PhabricatorProjectBoardController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $request->getViewer();
+ $id = $request->getURIData('id');
+
+ $column = id(new PhabricatorProjectColumnQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($id))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+ if (!$column) {
+ return new Aphront404Response();
+ }
+
+ $done_uri = $column->getWorkboardURI();
+
+ if (!$column->getTriggerPHID()) {
+ return $this->newDialog()
+ ->setTitle(pht('No Trigger'))
+ ->appendParagraph(
+ pht('This column does not have a trigger.'))
+ ->addCancelButton($done_uri);
+ }
+
+ if ($request->isFormPost()) {
+ $column_xactions = array();
+
+ $column_xactions[] = $column->getApplicationTransactionTemplate()
+ ->setTransactionType(
+ PhabricatorProjectColumnTriggerTransaction::TRANSACTIONTYPE)
+ ->setNewValue(null);
+
+ $column_editor = $column->getApplicationTransactionEditor()
+ ->setActor($viewer)
+ ->setContentSourceFromRequest($request)
+ ->setContinueOnNoEffect(true)
+ ->setContinueOnMissingFields(true);
+
+ $column_editor->applyTransactions($column, $column_xactions);
+
+ return id(new AphrontRedirectResponse())->setURI($done_uri);
+ }
+
+ $body = pht('Really remove the trigger from this column?');
+
+ return $this->newDialog()
+ ->setTitle(pht('Remove Trigger'))
+ ->appendParagraph($body)
+ ->addSubmitButton(pht('Remove Trigger'))
+ ->addCancelButton($done_uri);
+ }
+}
diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php
index 7c344b0b8..97e0ec9aa 100644
--- a/src/applications/project/controller/PhabricatorProjectController.php
+++ b/src/applications/project/controller/PhabricatorProjectController.php
@@ -1,177 +1,218 @@
<?php
abstract class PhabricatorProjectController extends PhabricatorController {
private $project;
private $profileMenu;
private $profileMenuEngine;
protected function setProject(PhabricatorProject $project) {
$this->project = $project;
return $this;
}
protected function getProject() {
return $this->project;
}
protected function loadProject() {
$viewer = $this->getViewer();
$request = $this->getRequest();
$id = nonempty(
$request->getURIData('projectID'),
$request->getURIData('id'));
$slug = $request->getURIData('slug');
if ($slug) {
$normal_slug = PhabricatorSlug::normalizeProjectSlug($slug);
$is_abnormal = ($slug !== $normal_slug);
$normal_uri = "/tag/{$normal_slug}/";
} else {
$is_abnormal = false;
}
$query = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->needMembers(true)
->needWatchers(true)
->needImages(true)
->needSlugs(true);
if ($slug) {
$query->withSlugs(array($slug));
} else {
$query->withIDs(array($id));
}
$policy_exception = null;
try {
$project = $query->executeOne();
} catch (PhabricatorPolicyException $ex) {
$policy_exception = $ex;
$project = null;
}
if (!$project) {
// This project legitimately does not exist, so just 404 the user.
if (!$policy_exception) {
return new Aphront404Response();
}
// Here, the project exists but the user can't see it. If they are
// using a non-canonical slug to view the project, redirect to the
// canonical slug. If they're already using the canonical slug, rethrow
// the exception to give them the policy error.
if ($is_abnormal) {
return id(new AphrontRedirectResponse())->setURI($normal_uri);
} else {
throw $policy_exception;
}
}
// The user can view the project, but is using a noncanonical slug.
// Redirect to the canonical slug.
$primary_slug = $project->getPrimarySlug();
if ($slug && ($slug !== $primary_slug)) {
$primary_uri = "/tag/{$primary_slug}/";
return id(new AphrontRedirectResponse())->setURI($primary_uri);
}
$this->setProject($project);
return null;
}
public function buildApplicationMenu() {
$menu = $this->newApplicationMenu();
$profile_menu = $this->getProfileMenu();
if ($profile_menu) {
$menu->setProfileMenu($profile_menu);
}
$menu->setSearchEngine(new PhabricatorProjectSearchEngine());
return $menu;
}
protected function getProfileMenu() {
if (!$this->profileMenu) {
$engine = $this->getProfileMenuEngine();
if ($engine) {
$this->profileMenu = $engine->buildNavigation();
}
}
return $this->profileMenu;
}
protected function buildApplicationCrumbs() {
+ return $this->newApplicationCrumbs('profile');
+ }
+
+ protected function newWorkboardCrumbs() {
+ return $this->newApplicationCrumbs('workboard');
+ }
+
+ private function newApplicationCrumbs($mode) {
$crumbs = parent::buildApplicationCrumbs();
$project = $this->getProject();
if ($project) {
$ancestors = $project->getAncestorProjects();
$ancestors = array_reverse($ancestors);
$ancestors[] = $project;
foreach ($ancestors as $ancestor) {
- $crumbs->addTextCrumb(
- $ancestor->getName(),
- $ancestor->getProfileURI()
- );
+ if ($ancestor->getPHID() === $project->getPHID()) {
+ // Link the current project's crumb to its profile no matter what,
+ // since we're already on the right context page for it and linking
+ // to the current page isn't helpful.
+ $crumb_uri = $ancestor->getProfileURI();
+ } else {
+ switch ($mode) {
+ case 'workboard':
+ $crumb_uri = $ancestor->getWorkboardURI();
+ break;
+ case 'profile':
+ default:
+ $crumb_uri = $ancestor->getProfileURI();
+ break;
+ }
+ }
+
+ $crumbs->addTextCrumb($ancestor->getName(), $crumb_uri);
}
}
return $crumbs;
}
protected function getProfileMenuEngine() {
if (!$this->profileMenuEngine) {
$viewer = $this->getViewer();
$project = $this->getProject();
if ($project) {
$engine = id(new PhabricatorProjectProfileMenuEngine())
->setViewer($viewer)
->setController($this)
->setProfileObject($project);
$this->profileMenuEngine = $engine;
}
}
return $this->profileMenuEngine;
}
protected function setProfileMenuEngine(
PhabricatorProjectProfileMenuEngine $engine) {
$this->profileMenuEngine = $engine;
return $this;
}
- protected function newCardResponse($board_phid, $object_phid) {
+ protected function newCardResponse(
+ $board_phid,
+ $object_phid,
+ PhabricatorProjectColumnOrder $ordering = null,
+ $sounds = array()) {
+
$viewer = $this->getViewer();
$request = $this->getRequest();
$visible_phids = $request->getStrList('visiblePHIDs');
if (!$visible_phids) {
$visible_phids = array();
}
- return id(new PhabricatorBoardResponseEngine())
+ $engine = id(new PhabricatorBoardResponseEngine())
->setViewer($viewer)
->setBoardPHID($board_phid)
->setObjectPHID($object_phid)
->setVisiblePHIDs($visible_phids)
- ->buildResponse();
+ ->setSounds($sounds);
+
+ if ($ordering) {
+ $engine->setOrdering($ordering);
+ }
+
+ return $engine->buildResponse();
+ }
+
+ public function renderHashtags(array $tags) {
+ $result = array();
+ foreach ($tags as $key => $tag) {
+ $result[] = '#'.$tag;
+ }
+ return implode(', ', $result);
}
public function renderHashtags(array $tags) {
$result = array();
foreach ($tags as $key => $tag) {
$result[] = '#'.$tag;
}
return implode(', ', $result);
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectDefaultController.php b/src/applications/project/controller/PhabricatorProjectDefaultController.php
index 8f42ff973..2c7a47b2d 100644
--- a/src/applications/project/controller/PhabricatorProjectDefaultController.php
+++ b/src/applications/project/controller/PhabricatorProjectDefaultController.php
@@ -1,90 +1,90 @@
<?php
final class PhabricatorProjectDefaultController
extends PhabricatorProjectBoardController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$project_id = $request->getURIData('projectID');
$project = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($project_id))
->executeOne();
if (!$project) {
return new Aphront404Response();
}
$this->setProject($project);
$target = $request->getURIData('target');
switch ($target) {
case 'filter':
$title = pht('Set Board Default Filter');
$body = pht(
'Make the current filter the new default filter for this board? '.
'All users will see the new filter as the default when they view '.
'the board.');
$button = pht('Save Default Filter');
$xaction_value = $request->getStr('filter');
$xaction_type = PhabricatorProjectFilterTransaction::TRANSACTIONTYPE;
break;
case 'sort':
$title = pht('Set Board Default Order');
$body = pht(
'Make the current sort order the new default order for this board? '.
'All users will see the new order as the default when they view '.
'the board.');
$button = pht('Save Default Order');
$xaction_value = $request->getStr('order');
$xaction_type = PhabricatorProjectSortTransaction::TRANSACTIONTYPE;
break;
default:
return new Aphront404Response();
}
$id = $project->getID();
$view_uri = $this->getApplicationURI("board/{$id}/");
$view_uri = new PhutilURI($view_uri);
foreach ($request->getPassthroughRequestData() as $key => $value) {
- $view_uri->setQueryParam($key, $value);
+ $view_uri->replaceQueryParam($key, $value);
}
if ($request->isFormPost()) {
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType($xaction_type)
->setNewValue($xaction_value);
id(new PhabricatorProjectTransactionEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($project, $xactions);
return id(new AphrontRedirectResponse())->setURI($view_uri);
}
$dialog = $this->newDialog()
->setTitle($title)
->appendChild($body)
->setDisableWorkflowOnCancel(true)
->addCancelButton($view_uri)
->addSubmitButton($title);
foreach ($request->getPassthroughRequestData() as $key => $value) {
$dialog->addHiddenInput($key, $value);
}
return $dialog;
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php
index 29b70cfaf..1fd8b3c67 100644
--- a/src/applications/project/controller/PhabricatorProjectMoveController.php
+++ b/src/applications/project/controller/PhabricatorProjectMoveController.php
@@ -1,210 +1,147 @@
<?php
final class PhabricatorProjectMoveController
extends PhabricatorProjectController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$request->validateCSRF();
$column_phid = $request->getStr('columnPHID');
$object_phid = $request->getStr('objectPHID');
- $after_phid = $request->getStr('afterPHID');
- $before_phid = $request->getStr('beforePHID');
- $order = $request->getStr('order', PhabricatorProjectColumn::DEFAULT_ORDER);
+
+ $after_phids = $request->getStrList('afterPHIDs');
+ $before_phids = $request->getStrList('beforePHIDs');
+
+ $order = $request->getStr('order');
+ if (!strlen($order)) {
+ $order = PhabricatorProjectColumnNaturalOrder::ORDERKEY;
+ }
+
+ $ordering = PhabricatorProjectColumnOrder::getOrderByKey($order);
+ $ordering = id(clone $ordering)
+ ->setViewer($viewer);
+
+ $edit_header = null;
+ $raw_header = $request->getStr('header');
+ if (strlen($raw_header)) {
+ $edit_header = phutil_json_decode($raw_header);
+ } else {
+ $edit_header = array();
+ }
$project = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
))
->withIDs(array($id))
->executeOne();
if (!$project) {
return new Aphront404Response();
}
+ $cancel_uri = $this->getApplicationURI(
+ new PhutilURI(
+ urisprintf('board/%d/', $project->getID()),
+ array(
+ 'order' => $order,
+ )));
+
$board_phid = $project->getPHID();
$object = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withPHIDs(array($object_phid))
->needProjectPHIDs(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$object) {
return new Aphront404Response();
}
$columns = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withProjectPHIDs(array($project->getPHID()))
+ ->needTriggers(true)
->execute();
$columns = mpull($columns, null, 'getPHID');
$column = idx($columns, $column_phid);
if (!$column) {
// User is trying to drop this object into a nonexistent column, just kick
// them out.
return new Aphront404Response();
}
$engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs(array($board_phid))
->setObjectPHIDs(array($object_phid))
->executeLayout();
- $columns = $engine->getObjectColumns($board_phid, $object_phid);
- $old_column_phids = mpull($columns, 'getPHID');
+ $order_params = array(
+ 'afterPHIDs' => $after_phids,
+ 'beforePHIDs' => $before_phids,
+ );
$xactions = array();
-
- $order_params = array();
- if ($order == PhabricatorProjectColumn::ORDER_NATURAL) {
- if ($after_phid) {
- $order_params['afterPHID'] = $after_phid;
- } else if ($before_phid) {
- $order_params['beforePHID'] = $before_phid;
- }
- }
-
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS)
->setNewValue(
array(
array(
'columnPHID' => $column->getPHID(),
) + $order_params,
));
- if ($order == PhabricatorProjectColumn::ORDER_PRIORITY) {
- $priority_xactions = $this->getPriorityTransactions(
- $object,
- $after_phid,
- $before_phid);
- foreach ($priority_xactions as $xaction) {
- $xactions[] = $xaction;
+ $header_xactions = $ordering->getColumnTransactions(
+ $object,
+ $edit_header);
+ foreach ($header_xactions as $header_xaction) {
+ $xactions[] = $header_xaction;
+ }
+
+ $sounds = array();
+ if ($column->canHaveTrigger()) {
+ $trigger = $column->getTrigger();
+ if ($trigger) {
+ $trigger_xactions = $trigger->newDropTransactions(
+ $viewer,
+ $column,
+ $object);
+ foreach ($trigger_xactions as $trigger_xaction) {
+ $xactions[] = $trigger_xaction;
+ }
+
+ foreach ($trigger->getSoundEffects() as $effect) {
+ $sounds[] = $effect;
+ }
}
}
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true)
- ->setContentSourceFromRequest($request);
+ ->setContentSourceFromRequest($request)
+ ->setCancelURI($cancel_uri);
$editor->applyTransactions($object, $xactions);
- return $this->newCardResponse($board_phid, $object_phid);
- }
-
- private function getPriorityTransactions(
- ManiphestTask $task,
- $after_phid,
- $before_phid) {
-
- list($after_task, $before_task) = $this->loadPriorityTasks(
- $after_phid,
- $before_phid);
-
- $must_move = false;
- if ($after_task && !$task->isLowerPriorityThan($after_task)) {
- $must_move = true;
- }
-
- if ($before_task && !$task->isHigherPriorityThan($before_task)) {
- $must_move = true;
- }
-
- // The move doesn't require a priority change to be valid, so don't
- // change the priority since we are not being forced to.
- if (!$must_move) {
- return array();
- }
-
- $try = array(
- array($after_task, true),
- array($before_task, false),
- );
-
- $pri = null;
- $sub = null;
- foreach ($try as $spec) {
- list($task, $is_after) = $spec;
-
- if (!$task) {
- continue;
- }
-
- list($pri, $sub) = ManiphestTransactionEditor::getAdjacentSubpriority(
- $task,
- $is_after);
-
- // If we find a priority on the first try, don't keep going.
- break;
- }
-
- $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap();
- $keyword = head(idx($keyword_map, $pri));
-
- $xactions = array();
- if ($pri !== null) {
- $xactions[] = id(new ManiphestTransaction())
- ->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE)
- ->setNewValue($keyword);
- $xactions[] = id(new ManiphestTransaction())
- ->setTransactionType(
- ManiphestTaskSubpriorityTransaction::TRANSACTIONTYPE)
- ->setNewValue($sub);
- }
-
- return $xactions;
- }
-
- private function loadPriorityTasks($after_phid, $before_phid) {
- $viewer = $this->getViewer();
-
- $task_phids = array();
-
- if ($after_phid) {
- $task_phids[] = $after_phid;
- }
- if ($before_phid) {
- $task_phids[] = $before_phid;
- }
-
- if (!$task_phids) {
- return array(null, null);
- }
-
- $tasks = id(new ManiphestTaskQuery())
- ->setViewer($viewer)
- ->withPHIDs($task_phids)
- ->execute();
- $tasks = mpull($tasks, null, 'getPHID');
-
- if ($after_phid) {
- $after_task = idx($tasks, $after_phid);
- } else {
- $after_task = null;
- }
-
- if ($before_phid) {
- $before_task = idx($tasks, $before_phid);
- } else {
- $before_task = null;
- }
-
- return array($after_task, $before_task);
+ return $this->newCardResponse(
+ $board_phid,
+ $object_phid,
+ $ordering,
+ $sounds);
}
}
diff --git a/src/applications/project/controller/trigger/PhabricatorProjectTriggerController.php b/src/applications/project/controller/trigger/PhabricatorProjectTriggerController.php
new file mode 100644
index 000000000..ea729e82a
--- /dev/null
+++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerController.php
@@ -0,0 +1,16 @@
+<?php
+
+abstract class PhabricatorProjectTriggerController
+ extends PhabricatorProjectController {
+
+ protected function buildApplicationCrumbs() {
+ $crumbs = parent::buildApplicationCrumbs();
+
+ $crumbs->addTextCrumb(
+ pht('Triggers'),
+ $this->getApplicationURI('trigger/'));
+
+ return $crumbs;
+ }
+
+}
diff --git a/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php b/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php
new file mode 100644
index 000000000..df362efb6
--- /dev/null
+++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php
@@ -0,0 +1,293 @@
+<?php
+
+final class PhabricatorProjectTriggerEditController
+ extends PhabricatorProjectTriggerController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $request = $this->getRequest();
+ $viewer = $request->getViewer();
+
+ $id = $request->getURIData('id');
+ if ($id) {
+ $trigger = id(new PhabricatorProjectTriggerQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($id))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+ if (!$trigger) {
+ return new Aphront404Response();
+ }
+ } else {
+ $trigger = PhabricatorProjectTrigger::initializeNewTrigger();
+ }
+
+ $column_phid = $request->getStr('columnPHID');
+ if ($column_phid) {
+ $column = id(new PhabricatorProjectColumnQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($column_phid))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+ if (!$column) {
+ return new Aphront404Response();
+ }
+ $board_uri = $column->getWorkboardURI();
+ } else {
+ $column = null;
+ $board_uri = null;
+ }
+
+ if ($board_uri) {
+ $cancel_uri = $board_uri;
+ } else if ($trigger->getID()) {
+ $cancel_uri = $trigger->getURI();
+ } else {
+ $cancel_uri = $this->getApplicationURI('trigger/');
+ }
+
+ $v_name = $trigger->getName();
+ $v_edit = $trigger->getEditPolicy();
+ $v_rules = $trigger->getTriggerRules();
+
+ $e_name = null;
+ $e_edit = null;
+
+ $validation_exception = null;
+ if ($request->isFormPost()) {
+ try {
+ $v_name = $request->getStr('name');
+ $v_edit = $request->getStr('editPolicy');
+
+ // Read the JSON rules from the request and convert them back into
+ // "TriggerRule" objects so we can render the correct form state
+ // if the user is modifying the rules
+ $raw_rules = $request->getStr('rules');
+ $raw_rules = phutil_json_decode($raw_rules);
+
+ $copy = clone $trigger;
+ $copy->setRuleset($raw_rules);
+ $v_rules = $copy->getTriggerRules();
+
+ $xactions = array();
+ if (!$trigger->getID()) {
+ $xactions[] = $trigger->getApplicationTransactionTemplate()
+ ->setTransactionType(PhabricatorTransactions::TYPE_CREATE)
+ ->setNewValue(true);
+ }
+
+ $xactions[] = $trigger->getApplicationTransactionTemplate()
+ ->setTransactionType(
+ PhabricatorProjectTriggerNameTransaction::TRANSACTIONTYPE)
+ ->setNewValue($v_name);
+
+ $xactions[] = $trigger->getApplicationTransactionTemplate()
+ ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
+ ->setNewValue($v_edit);
+
+ $xactions[] = $trigger->getApplicationTransactionTemplate()
+ ->setTransactionType(
+ PhabricatorProjectTriggerRulesetTransaction::TRANSACTIONTYPE)
+ ->setNewValue($raw_rules);
+
+ $editor = $trigger->getApplicationTransactionEditor()
+ ->setActor($viewer)
+ ->setContentSourceFromRequest($request)
+ ->setContinueOnNoEffect(true);
+
+ $editor->applyTransactions($trigger, $xactions);
+
+ $next_uri = $trigger->getURI();
+
+ if ($column) {
+ $column_xactions = array();
+
+ $column_xactions[] = $column->getApplicationTransactionTemplate()
+ ->setTransactionType(
+ PhabricatorProjectColumnTriggerTransaction::TRANSACTIONTYPE)
+ ->setNewValue($trigger->getPHID());
+
+ $column_editor = $column->getApplicationTransactionEditor()
+ ->setActor($viewer)
+ ->setContentSourceFromRequest($request)
+ ->setContinueOnNoEffect(true)
+ ->setContinueOnMissingFields(true);
+
+ $column_editor->applyTransactions($column, $column_xactions);
+
+ $next_uri = $column->getWorkboardURI();
+ }
+
+ return id(new AphrontRedirectResponse())->setURI($next_uri);
+ } catch (PhabricatorApplicationTransactionValidationException $ex) {
+ $validation_exception = $ex;
+
+ $e_name = $ex->getShortMessage(
+ PhabricatorProjectTriggerNameTransaction::TRANSACTIONTYPE);
+
+ $e_edit = $ex->getShortMessage(
+ PhabricatorTransactions::TYPE_EDIT_POLICY);
+
+ $trigger->setEditPolicy($v_edit);
+ }
+ }
+
+ if ($trigger->getID()) {
+ $title = $trigger->getObjectName();
+ $submit = pht('Save Trigger');
+ $header = pht('Edit Trigger: %s', $trigger->getObjectName());
+ } else {
+ $title = pht('New Trigger');
+ $submit = pht('Create Trigger');
+ $header = pht('New Trigger');
+ }
+
+ $form_id = celerity_generate_unique_node_id();
+ $table_id = celerity_generate_unique_node_id();
+ $create_id = celerity_generate_unique_node_id();
+ $input_id = celerity_generate_unique_node_id();
+
+ $form = id(new AphrontFormView())
+ ->setViewer($viewer)
+ ->setID($form_id);
+
+ if ($column) {
+ $form->addHiddenInput('columnPHID', $column->getPHID());
+ }
+
+ $form->appendControl(
+ id(new AphrontFormTextControl())
+ ->setLabel(pht('Name'))
+ ->setName('name')
+ ->setValue($v_name)
+ ->setError($e_name)
+ ->setPlaceholder($trigger->getDefaultName()));
+
+ $policies = id(new PhabricatorPolicyQuery())
+ ->setViewer($viewer)
+ ->setObject($trigger)
+ ->execute();
+
+ $form->appendControl(
+ id(new AphrontFormPolicyControl())
+ ->setName('editPolicy')
+ ->setPolicyObject($trigger)
+ ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
+ ->setPolicies($policies)
+ ->setError($e_edit));
+
+ $form->appendChild(
+ phutil_tag(
+ 'input',
+ array(
+ 'type' => 'hidden',
+ 'name' => 'rules',
+ 'id' => $input_id,
+ )));
+
+ $form->appendChild(
+ id(new PHUIFormInsetView())
+ ->setTitle(pht('Rules'))
+ ->setDescription(
+ pht(
+ 'When a card is dropped into a column which uses this trigger:'))
+ ->setRightButton(
+ javelin_tag(
+ 'a',
+ array(
+ 'href' => '#',
+ 'class' => 'button button-green',
+ 'id' => $create_id,
+ 'mustcapture' => true,
+ ),
+ pht('New Rule')))
+ ->setContent(
+ javelin_tag(
+ 'table',
+ array(
+ 'id' => $table_id,
+ 'class' => 'trigger-rules-table',
+ ))));
+
+ $this->setupEditorBehavior(
+ $trigger,
+ $v_rules,
+ $form_id,
+ $table_id,
+ $create_id,
+ $input_id);
+
+ $form->appendControl(
+ id(new AphrontFormSubmitControl())
+ ->setValue($submit)
+ ->addCancelButton($cancel_uri));
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader($header);
+
+ $box_view = id(new PHUIObjectBoxView())
+ ->setHeader($header)
+ ->setValidationException($validation_exception)
+ ->appendChild($form);
+
+ $column_view = id(new PHUITwoColumnView())
+ ->setFooter($box_view);
+
+ $crumbs = $this->buildApplicationCrumbs()
+ ->setBorder(true);
+
+ if ($column) {
+ $crumbs->addTextCrumb(
+ pht(
+ '%s: %s',
+ $column->getProject()->getDisplayName(),
+ $column->getName()),
+ $board_uri);
+ }
+
+ $crumbs->addTextCrumb($title);
+
+ return $this->newPage()
+ ->setTitle($title)
+ ->setCrumbs($crumbs)
+ ->appendChild($column_view);
+ }
+
+ private function setupEditorBehavior(
+ PhabricatorProjectTrigger $trigger,
+ array $rule_list,
+ $form_id,
+ $table_id,
+ $create_id,
+ $input_id) {
+
+ $rule_list = mpull($rule_list, 'toDictionary');
+ $rule_list = array_values($rule_list);
+
+ $type_list = PhabricatorProjectTriggerRule::getAllTriggerRules();
+ $type_list = mpull($type_list, 'newTemplate');
+ $type_list = array_values($type_list);
+
+ require_celerity_resource('project-triggers-css');
+
+ Javelin::initBehavior(
+ 'trigger-rule-editor',
+ array(
+ 'formNodeID' => $form_id,
+ 'tableNodeID' => $table_id,
+ 'createNodeID' => $create_id,
+ 'inputNodeID' => $input_id,
+
+ 'rules' => $rule_list,
+ 'types' => $type_list,
+ ));
+ }
+
+}
diff --git a/src/applications/project/controller/trigger/PhabricatorProjectTriggerListController.php b/src/applications/project/controller/trigger/PhabricatorProjectTriggerListController.php
new file mode 100644
index 000000000..62e5430f2
--- /dev/null
+++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerListController.php
@@ -0,0 +1,16 @@
+<?php
+
+final class PhabricatorProjectTriggerListController
+ extends PhabricatorProjectTriggerController {
+
+ public function shouldAllowPublic() {
+ return true;
+ }
+
+ public function handleRequest(AphrontRequest $request) {
+ return id(new PhabricatorProjectTriggerSearchEngine())
+ ->setController($this)
+ ->buildResponse();
+ }
+
+}
diff --git a/src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php b/src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php
new file mode 100644
index 000000000..d148c0a42
--- /dev/null
+++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php
@@ -0,0 +1,231 @@
+<?php
+
+final class PhabricatorProjectTriggerViewController
+ extends PhabricatorProjectTriggerController {
+
+ public function shouldAllowPublic() {
+ return true;
+ }
+
+ public function handleRequest(AphrontRequest $request) {
+ $request = $this->getRequest();
+ $viewer = $request->getViewer();
+
+ $id = $request->getURIData('id');
+
+ $trigger = id(new PhabricatorProjectTriggerQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($id))
+ ->executeOne();
+ if (!$trigger) {
+ return new Aphront404Response();
+ }
+
+ $rules_view = $this->newRulesView($trigger);
+ $columns_view = $this->newColumnsView($trigger);
+
+ $title = $trigger->getObjectName();
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader($trigger->getDisplayName());
+
+ $timeline = $this->buildTransactionTimeline(
+ $trigger,
+ new PhabricatorProjectTriggerTransactionQuery());
+ $timeline->setShouldTerminate(true);
+
+ $curtain = $this->newCurtain($trigger);
+
+ $column_view = id(new PHUITwoColumnView())
+ ->setHeader($header)
+ ->setCurtain($curtain)
+ ->setMainColumn(
+ array(
+ $rules_view,
+ $columns_view,
+ $timeline,
+ ));
+
+ $crumbs = $this->buildApplicationCrumbs()
+ ->addTextCrumb($trigger->getObjectName())
+ ->setBorder(true);
+
+ return $this->newPage()
+ ->setTitle($title)
+ ->setCrumbs($crumbs)
+ ->appendChild($column_view);
+ }
+
+ private function newColumnsView(PhabricatorProjectTrigger $trigger) {
+ $viewer = $this->getViewer();
+
+ // NOTE: When showing columns which use this trigger, we want to represent
+ // all columns the trigger is used by: even columns the user can't see.
+
+ // If we hide columns the viewer can't see, they might think that the
+ // trigger isn't widely used and is safe to edit, when it may actually
+ // be in use on workboards they don't have access to.
+
+ // Query the columns with the omnipotent viewer first, then pull out their
+ // PHIDs and throw the actual objects away. Re-query with the real viewer
+ // so we load only the columns they can actually see, but have a list of
+ // all the impacted column PHIDs.
+
+ // (We're also exposing the status of columns the user might not be able
+ // to see. This technically violates policy, but the trigger usage table
+ // hints at it anyway and it seems unlikely to ever have any security
+ // impact, but is useful in assessing whether a trigger is really in use
+ // or not.)
+
+ $omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
+ $all_columns = id(new PhabricatorProjectColumnQuery())
+ ->setViewer($omnipotent_viewer)
+ ->withTriggerPHIDs(array($trigger->getPHID()))
+ ->execute();
+ $column_map = mpull($all_columns, 'getStatus', 'getPHID');
+
+ if ($column_map) {
+ $visible_columns = id(new PhabricatorProjectColumnQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array_keys($column_map))
+ ->execute();
+ $visible_columns = mpull($visible_columns, null, 'getPHID');
+ } else {
+ $visible_columns = array();
+ }
+
+ $rows = array();
+ foreach ($column_map as $column_phid => $column_status) {
+ $column = idx($visible_columns, $column_phid);
+
+ if ($column) {
+ $project = $column->getProject();
+
+ $project_name = phutil_tag(
+ 'a',
+ array(
+ 'href' => $project->getURI(),
+ ),
+ $project->getDisplayName());
+
+ $column_name = phutil_tag(
+ 'a',
+ array(
+ 'href' => $column->getWorkboardURI(),
+ ),
+ $column->getDisplayName());
+ } else {
+ $project_name = null;
+ $column_name = phutil_tag('em', array(), pht('Restricted Column'));
+ }
+
+ if ($column_status == PhabricatorProjectColumn::STATUS_ACTIVE) {
+ $status_icon = id(new PHUIIconView())
+ ->setIcon('fa-columns', 'blue')
+ ->setTooltip(pht('Active Column'));
+ } else {
+ $status_icon = id(new PHUIIconView())
+ ->setIcon('fa-eye-slash', 'grey')
+ ->setTooltip(pht('Hidden Column'));
+ }
+
+ $rows[] = array(
+ $status_icon,
+ $project_name,
+ $column_name,
+ );
+ }
+
+ $table_view = id(new AphrontTableView($rows))
+ ->setNoDataString(pht('This trigger is not used by any columns.'))
+ ->setHeaders(
+ array(
+ null,
+ pht('Project'),
+ pht('Column'),
+ ))
+ ->setColumnClasses(
+ array(
+ null,
+ null,
+ 'wide pri',
+ ));
+
+ $header_view = id(new PHUIHeaderView())
+ ->setHeader(pht('Used by Columns'));
+
+ return id(new PHUIObjectBoxView())
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->setHeader($header_view)
+ ->setTable($table_view);
+ }
+
+ private function newRulesView(PhabricatorProjectTrigger $trigger) {
+ $viewer = $this->getViewer();
+ $rules = $trigger->getTriggerRules();
+
+ $rows = array();
+ foreach ($rules as $rule) {
+ $value = $rule->getRecord()->getValue();
+
+ $rows[] = array(
+ $rule->getRuleViewIcon($value),
+ $rule->getRuleViewLabel(),
+ $rule->getRuleViewDescription($value),
+ );
+ }
+
+ $table_view = id(new AphrontTableView($rows))
+ ->setNoDataString(pht('This trigger has no rules.'))
+ ->setHeaders(
+ array(
+ null,
+ pht('Rule'),
+ pht('Action'),
+ ))
+ ->setColumnClasses(
+ array(
+ null,
+ 'pri',
+ 'wide',
+ ));
+
+ $header_view = id(new PHUIHeaderView())
+ ->setHeader(pht('Trigger Rules'))
+ ->setSubheader(
+ pht(
+ 'When a card is dropped into a column that uses this trigger, '.
+ 'these actions will be taken.'));
+
+ return id(new PHUIObjectBoxView())
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->setHeader($header_view)
+ ->setTable($table_view);
+ }
+ private function newCurtain(PhabricatorProjectTrigger $trigger) {
+ $viewer = $this->getViewer();
+
+ $can_edit = PhabricatorPolicyFilter::hasCapability(
+ $viewer,
+ $trigger,
+ PhabricatorPolicyCapability::CAN_EDIT);
+
+ $curtain = $this->newCurtainView($trigger);
+
+ $edit_uri = $this->getApplicationURI(
+ urisprintf(
+ 'trigger/edit/%d/',
+ $trigger->getID()));
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setName(pht('Edit Trigger'))
+ ->setIcon('fa-pencil')
+ ->setHref($edit_uri)
+ ->setDisabled(!$can_edit)
+ ->setWorkflow(!$can_edit));
+
+ return $curtain;
+ }
+
+}
diff --git a/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php
index d49476708..e0becc347 100644
--- a/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php
+++ b/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php
@@ -1,140 +1,22 @@
<?php
final class PhabricatorProjectColumnTransactionEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorProjectApplication';
}
public function getEditorObjectsDescription() {
return pht('Workboard Columns');
}
- public function getTransactionTypes() {
- $types = parent::getTransactionTypes();
-
- $types[] = PhabricatorProjectColumnTransaction::TYPE_NAME;
- $types[] = PhabricatorProjectColumnTransaction::TYPE_STATUS;
- $types[] = PhabricatorProjectColumnTransaction::TYPE_LIMIT;
-
- return $types;
- }
-
- protected function getCustomTransactionOldValue(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
-
- switch ($xaction->getTransactionType()) {
- case PhabricatorProjectColumnTransaction::TYPE_NAME:
- return $object->getName();
- case PhabricatorProjectColumnTransaction::TYPE_STATUS:
- return $object->getStatus();
- case PhabricatorProjectColumnTransaction::TYPE_LIMIT:
- return $object->getPointLimit();
-
- }
-
- return parent::getCustomTransactionOldValue($object, $xaction);
+ public function getCreateObjectTitle($author, $object) {
+ return pht('%s created this column.', $author);
}
- protected function getCustomTransactionNewValue(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
-
- switch ($xaction->getTransactionType()) {
- case PhabricatorProjectColumnTransaction::TYPE_NAME:
- case PhabricatorProjectColumnTransaction::TYPE_STATUS:
- return $xaction->getNewValue();
- case PhabricatorProjectColumnTransaction::TYPE_LIMIT:
- $value = $xaction->getNewValue();
- if (strlen($value)) {
- return (int)$xaction->getNewValue();
- } else {
- return null;
- }
- }
-
- return parent::getCustomTransactionNewValue($object, $xaction);
- }
-
- protected function applyCustomInternalTransaction(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
-
- switch ($xaction->getTransactionType()) {
- case PhabricatorProjectColumnTransaction::TYPE_NAME:
- $object->setName($xaction->getNewValue());
- return;
- case PhabricatorProjectColumnTransaction::TYPE_STATUS:
- $object->setStatus($xaction->getNewValue());
- return;
- case PhabricatorProjectColumnTransaction::TYPE_LIMIT:
- $object->setPointLimit($xaction->getNewValue());
- return;
- }
-
- return parent::applyCustomInternalTransaction($object, $xaction);
- }
-
- protected function applyCustomExternalTransaction(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
-
- switch ($xaction->getTransactionType()) {
- case PhabricatorProjectColumnTransaction::TYPE_NAME:
- case PhabricatorProjectColumnTransaction::TYPE_STATUS:
- case PhabricatorProjectColumnTransaction::TYPE_LIMIT:
- return;
- }
-
- return parent::applyCustomExternalTransaction($object, $xaction);
- }
-
- protected function validateTransaction(
- PhabricatorLiskDAO $object,
- $type,
- array $xactions) {
-
- $errors = parent::validateTransaction($object, $type, $xactions);
-
- switch ($type) {
- case PhabricatorProjectColumnTransaction::TYPE_LIMIT:
- foreach ($xactions as $xaction) {
- $value = $xaction->getNewValue();
- if (strlen($value) && !preg_match('/^\d+\z/', $value)) {
- $errors[] = new PhabricatorApplicationTransactionValidationError(
- $type,
- pht('Invalid'),
- pht(
- 'Column point limit must either be empty or a nonnegative '.
- 'integer.'),
- $xaction);
- }
- }
- break;
- case PhabricatorProjectColumnTransaction::TYPE_NAME:
- $missing = $this->validateIsEmptyTextField(
- $object->getName(),
- $xactions);
-
- // The default "Backlog" column is allowed to be unnamed, which
- // means we use the default name.
-
- if ($missing && !$object->isDefaultColumn()) {
- $error = new PhabricatorApplicationTransactionValidationError(
- $type,
- pht('Required'),
- pht('Column name is required.'),
- nonempty(last($xactions), null));
-
- $error->setIsMissingFieldError(true);
- $errors[] = $error;
- }
- break;
- }
-
- return $errors;
+ public function getCreateObjectTitleForFeed($author, $object) {
+ return pht('%s created %s.', $author, $object);
}
}
diff --git a/src/applications/project/editor/PhabricatorProjectTriggerEditor.php b/src/applications/project/editor/PhabricatorProjectTriggerEditor.php
new file mode 100644
index 000000000..9014fd6f1
--- /dev/null
+++ b/src/applications/project/editor/PhabricatorProjectTriggerEditor.php
@@ -0,0 +1,34 @@
+<?php
+
+final class PhabricatorProjectTriggerEditor
+ extends PhabricatorApplicationTransactionEditor {
+
+ public function getEditorApplicationClass() {
+ return 'PhabricatorProjectApplication';
+ }
+
+ public function getEditorObjectsDescription() {
+ return pht('Triggers');
+ }
+
+ public function getCreateObjectTitle($author, $object) {
+ return pht('%s created this trigger.', $author);
+ }
+
+ public function getCreateObjectTitleForFeed($author, $object) {
+ return pht('%s created %s.', $author, $object);
+ }
+
+ public function getTransactionTypes() {
+ $types = parent::getTransactionTypes();
+
+ $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
+
+ return $types;
+ }
+
+ protected function supportsSearch() {
+ return true;
+ }
+
+}
diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php
index 863f8c0e8..e614ec2f9 100644
--- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php
+++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php
@@ -1,612 +1,595 @@
<?php
final class PhabricatorBoardLayoutEngine extends Phobject {
private $viewer;
private $boardPHIDs;
private $objectPHIDs;
private $boards;
private $columnMap = array();
private $objectColumnMap = array();
private $boardLayout = array();
private $fetchAllBoards;
private $remQueue = array();
private $addQueue = array();
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setBoardPHIDs(array $board_phids) {
$this->boardPHIDs = array_fuse($board_phids);
return $this;
}
public function getBoardPHIDs() {
return $this->boardPHIDs;
}
public function setObjectPHIDs(array $object_phids) {
$this->objectPHIDs = array_fuse($object_phids);
return $this;
}
public function getObjectPHIDs() {
return $this->objectPHIDs;
}
/**
* Fetch all boards, even if the board is disabled.
*/
public function setFetchAllBoards($fetch_all) {
$this->fetchAllBoards = $fetch_all;
return $this;
}
public function getFetchAllBoards() {
return $this->fetchAllBoards;
}
public function executeLayout() {
$viewer = $this->getViewer();
$boards = $this->loadBoards();
if (!$boards) {
return $this;
}
$columns = $this->loadColumns($boards);
$positions = $this->loadPositions($boards);
foreach ($boards as $board_phid => $board) {
$board_columns = idx($columns, $board_phid);
// Don't layout boards with no columns. These boards need to be formally
// created first.
if (!$columns) {
continue;
}
$board_positions = idx($positions, $board_phid, array());
$this->layoutBoard($board, $board_columns, $board_positions);
}
return $this;
}
public function getColumns($board_phid) {
$columns = idx($this->boardLayout, $board_phid, array());
return array_select_keys($this->columnMap, array_keys($columns));
}
public function getColumnObjectPositions($board_phid, $column_phid) {
$columns = idx($this->boardLayout, $board_phid, array());
return idx($columns, $column_phid, array());
}
public function getColumnObjectPHIDs($board_phid, $column_phid) {
$positions = $this->getColumnObjectPositions($board_phid, $column_phid);
return mpull($positions, 'getObjectPHID');
}
public function getObjectColumns($board_phid, $object_phid) {
$board_map = idx($this->objectColumnMap, $board_phid, array());
$column_phids = idx($board_map, $object_phid);
if (!$column_phids) {
return array();
}
return array_select_keys($this->columnMap, $column_phids);
}
public function queueRemovePosition(
$board_phid,
$column_phid,
$object_phid) {
$board_layout = idx($this->boardLayout, $board_phid, array());
$positions = idx($board_layout, $column_phid, array());
$position = idx($positions, $object_phid);
if ($position) {
$this->remQueue[] = $position;
// If this position hasn't been saved yet, get it out of the add queue.
if (!$position->getID()) {
foreach ($this->addQueue as $key => $add_position) {
if ($add_position === $position) {
unset($this->addQueue[$key]);
}
}
}
}
unset($this->boardLayout[$board_phid][$column_phid][$object_phid]);
return $this;
}
- public function queueAddPositionBefore(
- $board_phid,
- $column_phid,
- $object_phid,
- $before_phid) {
-
- return $this->queueAddPositionRelative(
- $board_phid,
- $column_phid,
- $object_phid,
- $before_phid,
- true);
- }
-
- public function queueAddPositionAfter(
- $board_phid,
- $column_phid,
- $object_phid,
- $after_phid) {
-
- return $this->queueAddPositionRelative(
- $board_phid,
- $column_phid,
- $object_phid,
- $after_phid,
- false);
- }
-
public function queueAddPosition(
- $board_phid,
- $column_phid,
- $object_phid) {
- return $this->queueAddPositionRelative(
- $board_phid,
- $column_phid,
- $object_phid,
- null,
- true);
- }
-
- private function queueAddPositionRelative(
$board_phid,
$column_phid,
$object_phid,
- $relative_phid,
- $is_before) {
+ array $after_phids,
+ array $before_phids) {
$board_layout = idx($this->boardLayout, $board_phid, array());
$positions = idx($board_layout, $column_phid, array());
// Check if the object is already in the column, and remove it if it is.
$object_position = idx($positions, $object_phid);
unset($positions[$object_phid]);
if (!$object_position) {
$object_position = id(new PhabricatorProjectColumnPosition())
->setBoardPHID($board_phid)
->setColumnPHID($column_phid)
->setObjectPHID($object_phid);
}
- $found = false;
if (!$positions) {
$object_position->setSequence(0);
} else {
- foreach ($positions as $position) {
- if (!$found) {
- if ($relative_phid === null) {
- $is_match = true;
- } else {
- $position_phid = $position->getObjectPHID();
- $is_match = ($relative_phid == $position_phid);
- }
-
- if ($is_match) {
- $found = true;
+ // The user's view of the board may fall out of date, so they might
+ // try to drop a card under a different card which is no longer where
+ // they thought it was.
+
+ // When this happens, we perform the move anyway, since this is almost
+ // certainly what users want when interacting with the UI. We'l try to
+ // fall back to another nearby card if the client provided us one. If
+ // we don't find any of the cards the client specified in the column,
+ // we'll just move the card to the default position.
+
+ $search_phids = array();
+ foreach ($after_phids as $after_phid) {
+ $search_phids[] = array($after_phid, false);
+ }
- $sequence = $position->getSequence();
+ foreach ($before_phids as $before_phid) {
+ $search_phids[] = array($before_phid, true);
+ }
- if (!$is_before) {
- $sequence++;
+ // This makes us fall back to the default position if we fail every
+ // candidate position. The default position counts as a "before" position
+ // because we want to put the new card at the top of the column.
+ $search_phids[] = array(null, true);
+
+ $found = false;
+ foreach ($search_phids as $search_position) {
+ list($relative_phid, $is_before) = $search_position;
+ foreach ($positions as $position) {
+ if (!$found) {
+ if ($relative_phid === null) {
+ $is_match = true;
+ } else {
+ $position_phid = $position->getObjectPHID();
+ $is_match = ($relative_phid === $position_phid);
}
- $object_position->setSequence($sequence++);
+ if ($is_match) {
+ $found = true;
- if (!$is_before) {
- // If we're inserting after this position, continue the loop so
- // we don't update it.
- continue;
+ $sequence = $position->getSequence();
+
+ if (!$is_before) {
+ $sequence++;
+ }
+
+ $object_position->setSequence($sequence++);
+
+ if (!$is_before) {
+ // If we're inserting after this position, continue the loop so
+ // we don't update it.
+ continue;
+ }
}
}
+
+ if ($found) {
+ $position->setSequence($sequence++);
+ $this->addQueue[] = $position;
+ }
}
if ($found) {
- $position->setSequence($sequence++);
- $this->addQueue[] = $position;
+ break;
}
}
}
- if ($relative_phid && !$found) {
- throw new Exception(
- pht(
- 'Unable to find object "%s" in column "%s" on board "%s".',
- $relative_phid,
- $column_phid,
- $board_phid));
- }
-
$this->addQueue[] = $object_position;
$positions[$object_phid] = $object_position;
$positions = msort($positions, 'getOrderingKey');
$this->boardLayout[$board_phid][$column_phid] = $positions;
return $this;
}
public function applyPositionUpdates() {
foreach ($this->remQueue as $position) {
if ($position->getID()) {
$position->delete();
}
}
$this->remQueue = array();
$adds = array();
$updates = array();
foreach ($this->addQueue as $position) {
$id = $position->getID();
if ($id) {
$updates[$id] = $position;
} else {
$adds[] = $position;
}
}
$this->addQueue = array();
$table = new PhabricatorProjectColumnPosition();
$conn_w = $table->establishConnection('w');
$pairs = array();
foreach ($updates as $id => $position) {
// This is ugly because MySQL gets upset with us if it is configured
// strictly and we attempt inserts which can't work. We'll never actually
// do these inserts since they'll always collide (triggering the ON
// DUPLICATE KEY logic), so we just provide dummy values in order to get
// there.
$pairs[] = qsprintf(
$conn_w,
'(%d, %d, "", "", "")',
$id,
$position->getSequence());
}
if ($pairs) {
queryfx(
$conn_w,
'INSERT INTO %T (id, sequence, boardPHID, columnPHID, objectPHID)
VALUES %LQ ON DUPLICATE KEY UPDATE sequence = VALUES(sequence)',
$table->getTableName(),
$pairs);
}
foreach ($adds as $position) {
$position->save();
}
return $this;
}
private function loadBoards() {
$viewer = $this->getViewer();
$board_phids = $this->getBoardPHIDs();
$boards = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs($board_phids)
->execute();
$boards = mpull($boards, null, 'getPHID');
if (!$this->fetchAllBoards) {
foreach ($boards as $key => $board) {
if (!$board->getHasWorkboard()) {
unset($boards[$key]);
}
}
}
return $boards;
}
private function loadColumns(array $boards) {
$viewer = $this->getViewer();
$columns = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withProjectPHIDs(array_keys($boards))
+ ->needTriggers(true)
->execute();
$columns = msort($columns, 'getOrderingKey');
$columns = mpull($columns, null, 'getPHID');
$need_children = array();
foreach ($boards as $phid => $board) {
if ($board->getHasMilestones() || $board->getHasSubprojects()) {
$need_children[] = $phid;
}
}
if ($need_children) {
$children = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withParentProjectPHIDs($need_children)
->execute();
$children = mpull($children, null, 'getPHID');
$children = mgroup($children, 'getParentProjectPHID');
} else {
$children = array();
}
$columns = mgroup($columns, 'getProjectPHID');
foreach ($boards as $board_phid => $board) {
$board_columns = idx($columns, $board_phid, array());
// If the project has milestones, create any missing columns.
if ($board->getHasMilestones() || $board->getHasSubprojects()) {
$child_projects = idx($children, $board_phid, array());
if ($board_columns) {
$next_sequence = last($board_columns)->getSequence() + 1;
} else {
$next_sequence = 1;
}
$proxy_columns = mpull($board_columns, null, 'getProxyPHID');
foreach ($child_projects as $child_phid => $child) {
if (isset($proxy_columns[$child_phid])) {
continue;
}
$new_column = PhabricatorProjectColumn::initializeNewColumn($viewer)
->attachProject($board)
->attachProxy($child)
->setSequence($next_sequence++)
->setProjectPHID($board_phid)
->setProxyPHID($child_phid);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$new_column->save();
unset($unguarded);
$board_columns[$new_column->getPHID()] = $new_column;
}
}
$board_columns = msort($board_columns, 'getOrderingKey');
$columns[$board_phid] = $board_columns;
}
foreach ($columns as $board_phid => $board_columns) {
foreach ($board_columns as $board_column) {
$column_phid = $board_column->getPHID();
$this->columnMap[$column_phid] = $board_column;
}
}
return $columns;
}
private function loadPositions(array $boards) {
$viewer = $this->getViewer();
$object_phids = $this->getObjectPHIDs();
if (!$object_phids) {
return array();
}
$positions = id(new PhabricatorProjectColumnPositionQuery())
->setViewer($viewer)
->withBoardPHIDs(array_keys($boards))
->withObjectPHIDs($object_phids)
->execute();
$positions = msort($positions, 'getOrderingKey');
$positions = mgroup($positions, 'getBoardPHID');
return $positions;
}
private function layoutBoard(
$board,
array $columns,
array $positions) {
$viewer = $this->getViewer();
$board_phid = $board->getPHID();
$position_groups = mgroup($positions, 'getObjectPHID');
$layout = array();
$default_phid = null;
foreach ($columns as $column) {
$column_phid = $column->getPHID();
$layout[$column_phid] = array();
if ($column->isDefaultColumn()) {
$default_phid = $column_phid;
}
}
// Find all the columns which are proxies for other objects.
$proxy_map = array();
foreach ($columns as $column) {
$proxy_phid = $column->getProxyPHID();
if ($proxy_phid) {
$proxy_map[$proxy_phid] = $column->getPHID();
}
}
$object_phids = $this->getObjectPHIDs();
// If we have proxies, we need to force cards into the correct proxy
// columns.
if ($proxy_map && $object_phids) {
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($object_phids)
->withEdgeTypes(
array(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
));
$edge_query->execute();
$project_phids = $edge_query->getDestinationPHIDs();
$project_phids = array_fuse($project_phids);
} else {
$project_phids = array();
}
if ($project_phids) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withPHIDs($project_phids)
->execute();
$projects = mpull($projects, null, 'getPHID');
} else {
$projects = array();
}
// Build a map from every project that any task is tagged with to the
// ancestor project which has a column on this board, if one exists.
$ancestor_map = array();
foreach ($projects as $phid => $project) {
if (isset($proxy_map[$phid])) {
$ancestor_map[$phid] = $proxy_map[$phid];
} else {
$seen = array($phid);
foreach ($project->getAncestorProjects() as $ancestor) {
$ancestor_phid = $ancestor->getPHID();
$seen[] = $ancestor_phid;
if (isset($proxy_map[$ancestor_phid])) {
foreach ($seen as $project_phid) {
$ancestor_map[$project_phid] = $proxy_map[$ancestor_phid];
}
}
}
}
}
$view_sequence = 1;
foreach ($object_phids as $object_phid) {
$positions = idx($position_groups, $object_phid, array());
// First, check for objects that have corresponding proxy columns. We're
// going to overwrite normal column positions if a tag belongs to a proxy
// column, since you can't be in normal columns if you're in proxy
// columns.
$proxy_hits = array();
if ($proxy_map) {
$object_project_phids = $edge_query->getDestinationPHIDs(
array(
$object_phid,
));
foreach ($object_project_phids as $project_phid) {
if (isset($ancestor_map[$project_phid])) {
$proxy_hits[] = $ancestor_map[$project_phid];
}
}
}
if ($proxy_hits) {
// TODO: For now, only one column hit is permissible.
$proxy_hits = array_slice($proxy_hits, 0, 1);
$proxy_hits = array_fuse($proxy_hits);
// Check the object positions: we hope to find a position in each
// column the object should be part of. We're going to drop any
// invalid positions and create new positions where positions are
// missing.
foreach ($positions as $key => $position) {
$column_phid = $position->getColumnPHID();
if (isset($proxy_hits[$column_phid])) {
// Valid column, mark the position as found.
unset($proxy_hits[$column_phid]);
} else {
// Invalid column, ignore the position.
unset($positions[$key]);
}
}
// Create new positions for anything we haven't found.
foreach ($proxy_hits as $proxy_hit) {
$new_position = id(new PhabricatorProjectColumnPosition())
->setBoardPHID($board_phid)
->setColumnPHID($proxy_hit)
->setObjectPHID($object_phid)
->setSequence(0)
->setViewSequence($view_sequence++);
$this->addQueue[] = $new_position;
$positions[] = $new_position;
}
} else {
// Ignore any positions in columns which no longer exist. We don't
// actively destory them because the rest of the code ignores them and
// there's no real need to destroy the data.
foreach ($positions as $key => $position) {
$column_phid = $position->getColumnPHID();
if (empty($columns[$column_phid])) {
unset($positions[$key]);
}
}
// If the object has no position, put it on the default column if
// one exists.
if (!$positions && $default_phid) {
$new_position = id(new PhabricatorProjectColumnPosition())
->setBoardPHID($board_phid)
->setColumnPHID($default_phid)
->setObjectPHID($object_phid)
->setSequence(0)
->setViewSequence($view_sequence++);
$this->addQueue[] = $new_position;
$positions = array(
$new_position,
);
}
}
foreach ($positions as $position) {
$column_phid = $position->getColumnPHID();
$layout[$column_phid][$object_phid] = $position;
}
}
foreach ($layout as $column_phid => $map) {
$map = msort($map, 'getOrderingKey');
$layout[$column_phid] = $map;
foreach ($map as $object_phid => $position) {
$this->objectColumnMap[$board_phid][$object_phid][] = $column_phid;
}
}
$this->boardLayout[$board_phid] = $layout;
}
}
diff --git a/src/applications/project/engine/PhabricatorBoardRenderingEngine.php b/src/applications/project/engine/PhabricatorBoardRenderingEngine.php
index d76497bc2..f5a81eb9b 100644
--- a/src/applications/project/engine/PhabricatorBoardRenderingEngine.php
+++ b/src/applications/project/engine/PhabricatorBoardRenderingEngine.php
@@ -1,146 +1,147 @@
<?php
final class PhabricatorBoardRenderingEngine extends Phobject {
private $viewer;
private $objects;
private $excludedProjectPHIDs;
private $editMap;
private $loaded;
private $handles;
private $coverFiles;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setObjects(array $objects) {
$this->objects = mpull($objects, null, 'getPHID');
return $this;
}
public function getObjects() {
return $this->objects;
}
public function setExcludedProjectPHIDs(array $phids) {
$this->excludedProjectPHIDs = $phids;
return $this;
}
public function getExcludedProjectPHIDs() {
return $this->excludedProjectPHIDs;
}
public function setEditMap(array $edit_map) {
$this->editMap = $edit_map;
return $this;
}
public function getEditMap() {
return $this->editMap;
}
public function renderCard($phid) {
$this->willRender();
$viewer = $this->getViewer();
$object = idx($this->getObjects(), $phid);
$card = id(new ProjectBoardTaskCard())
->setViewer($viewer)
->setTask($object)
+ ->setShowEditControls(true)
->setCanEdit($this->getCanEdit($phid));
$owner_phid = $object->getOwnerPHID();
if ($owner_phid) {
$owner_handle = $this->handles[$owner_phid];
$card->setOwner($owner_handle);
}
$project_phids = $object->getProjectPHIDs();
$project_handles = array_select_keys($this->handles, $project_phids);
if ($project_handles) {
$card
->setHideArchivedProjects(true)
->setProjectHandles($project_handles);
}
$cover_phid = $object->getCoverImageThumbnailPHID();
if ($cover_phid) {
$cover_file = idx($this->coverFiles, $cover_phid);
if ($cover_file) {
$card->setCoverImageFile($cover_file);
}
}
return $card;
}
private function willRender() {
if ($this->loaded) {
return;
}
$phids = array();
foreach ($this->objects as $object) {
$owner_phid = $object->getOwnerPHID();
if ($owner_phid) {
$phids[$owner_phid] = $owner_phid;
}
foreach ($object->getProjectPHIDs() as $phid) {
$phids[$phid] = $phid;
}
}
if ($this->excludedProjectPHIDs) {
foreach ($this->excludedProjectPHIDs as $excluded_phid) {
unset($phids[$excluded_phid]);
}
}
$viewer = $this->getViewer();
$handles = $viewer->loadHandles($phids);
$handles = iterator_to_array($handles);
$this->handles = $handles;
$cover_phids = array();
foreach ($this->objects as $object) {
$cover_phid = $object->getCoverImageThumbnailPHID();
if ($cover_phid) {
$cover_phids[$cover_phid] = $cover_phid;
}
}
if ($cover_phids) {
$cover_files = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs($cover_phids)
->execute();
$cover_files = mpull($cover_files, null, 'getPHID');
} else {
$cover_files = array();
}
$this->coverFiles = $cover_files;
$this->loaded = true;
}
private function getCanEdit($phid) {
if ($this->editMap === null) {
return true;
}
return idx($this->editMap, $phid);
}
}
diff --git a/src/applications/project/engine/PhabricatorBoardResponseEngine.php b/src/applications/project/engine/PhabricatorBoardResponseEngine.php
index 969dfa3bc..f22254e43 100644
--- a/src/applications/project/engine/PhabricatorBoardResponseEngine.php
+++ b/src/applications/project/engine/PhabricatorBoardResponseEngine.php
@@ -1,149 +1,220 @@
<?php
final class PhabricatorBoardResponseEngine extends Phobject {
private $viewer;
private $boardPHID;
private $objectPHID;
private $visiblePHIDs;
+ private $ordering;
+ private $sounds;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setBoardPHID($board_phid) {
$this->boardPHID = $board_phid;
return $this;
}
public function getBoardPHID() {
return $this->boardPHID;
}
public function setObjectPHID($object_phid) {
$this->objectPHID = $object_phid;
return $this;
}
public function getObjectPHID() {
return $this->objectPHID;
}
public function setVisiblePHIDs(array $visible_phids) {
$this->visiblePHIDs = $visible_phids;
return $this;
}
public function getVisiblePHIDs() {
return $this->visiblePHIDs;
}
+ public function setOrdering(PhabricatorProjectColumnOrder $ordering) {
+ $this->ordering = $ordering;
+ return $this;
+ }
+
+ public function getOrdering() {
+ return $this->ordering;
+ }
+
+ public function setSounds(array $sounds) {
+ $this->sounds = $sounds;
+ return $this;
+ }
+
+ public function getSounds() {
+ return $this->sounds;
+ }
+
public function buildResponse() {
$viewer = $this->getViewer();
$object_phid = $this->getObjectPHID();
$board_phid = $this->getBoardPHID();
+ $ordering = $this->getOrdering();
// Load all the other tasks that are visible in the affected columns and
// perform layout for them.
$visible_phids = $this->getAllVisiblePHIDs();
$layout_engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs(array($board_phid))
->setObjectPHIDs($visible_phids)
->executeLayout();
$object_columns = $layout_engine->getObjectColumns(
$board_phid,
$object_phid);
$natural = array();
foreach ($object_columns as $column_phid => $column) {
$column_object_phids = $layout_engine->getColumnObjectPHIDs(
$board_phid,
$column_phid);
$natural[$column_phid] = array_values($column_object_phids);
}
$all_visible = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withPHIDs($visible_phids)
->execute();
-
- $order_maps = array();
- foreach ($all_visible as $visible) {
- $order_maps[$visible->getPHID()] = $visible->getWorkboardOrderVectors();
+ $all_visible = mpull($all_visible, null, 'getPHID');
+
+ if ($ordering) {
+ $vectors = $ordering->getSortVectorsForObjects($all_visible);
+ $header_keys = $ordering->getHeaderKeysForObjects($all_visible);
+ $headers = $ordering->getHeadersForObjects($all_visible);
+ $headers = mpull($headers, 'toDictionary');
+ } else {
+ $vectors = array();
+ $header_keys = array();
+ $headers = array();
}
$object = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withPHIDs(array($object_phid))
->needProjectPHIDs(true)
->executeOne();
if (!$object) {
return new Aphront404Response();
}
$template = $this->buildTemplate($object);
+ $cards = array();
+ foreach ($all_visible as $card_phid => $object) {
+ $card = array(
+ 'vectors' => array(),
+ 'headers' => array(),
+ 'properties' => array(),
+ 'nodeHTMLTemplate' => null,
+ );
+
+ if ($ordering) {
+ $order_key = $ordering->getColumnOrderKey();
+
+ $vector = idx($vectors, $card_phid);
+ if ($vector !== null) {
+ $card['vectors'][$order_key] = $vector;
+ }
+
+ $header = idx($header_keys, $card_phid);
+ if ($header !== null) {
+ $card['headers'][$order_key] = $header;
+ }
+
+ $card['properties'] = self::newTaskProperties($object);
+ }
+
+ if ($card_phid === $object_phid) {
+ $card['nodeHTMLTemplate'] = hsprintf('%s', $template);
+ }
+
+ $card['vectors'] = (object)$card['vectors'];
+ $card['headers'] = (object)$card['headers'];
+ $card['properties'] = (object)$card['properties'];
+
+ $cards[$card_phid] = $card;
+ }
+
$payload = array(
'objectPHID' => $object_phid,
- 'cardHTML' => $template,
'columnMaps' => $natural,
- 'orderMaps' => $order_maps,
- 'propertyMaps' => array(
- $object_phid => $object->getWorkboardProperties(),
- ),
+ 'cards' => $cards,
+ 'headers' => $headers,
+ 'sounds' => $this->getSounds(),
);
return id(new AphrontAjaxResponse())
->setContent($payload);
}
+ public static function newTaskProperties($task) {
+ return array(
+ 'points' => (double)$task->getPoints(),
+ 'status' => $task->getStatus(),
+ 'priority' => (int)$task->getPriority(),
+ 'owner' => $task->getOwnerPHID(),
+ );
+ }
+
private function buildTemplate($object) {
$viewer = $this->getViewer();
$object_phid = $this->getObjectPHID();
$excluded_phids = $this->loadExcludedProjectPHIDs();
$rendering_engine = id(new PhabricatorBoardRenderingEngine())
->setViewer($viewer)
->setObjects(array($object))
->setExcludedProjectPHIDs($excluded_phids);
$card = $rendering_engine->renderCard($object_phid);
return hsprintf('%s', $card->getItem());
}
private function loadExcludedProjectPHIDs() {
$viewer = $this->getViewer();
$board_phid = $this->getBoardPHID();
$exclude_phids = array($board_phid);
$descendants = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withAncestorProjectPHIDs($exclude_phids)
->execute();
foreach ($descendants as $descendant) {
$exclude_phids[] = $descendant->getPHID();
}
return array_fuse($exclude_phids);
}
private function getAllVisiblePHIDs() {
$visible_phids = $this->getVisiblePHIDs();
$visible_phids[] = $this->getObjectPHID();
$visible_phids = array_fuse($visible_phids);
return $visible_phids;
}
}
diff --git a/src/applications/project/engineextension/PhabricatorProjectTriggerUsageIndexEngineExtension.php b/src/applications/project/engineextension/PhabricatorProjectTriggerUsageIndexEngineExtension.php
new file mode 100644
index 000000000..b50c51fba
--- /dev/null
+++ b/src/applications/project/engineextension/PhabricatorProjectTriggerUsageIndexEngineExtension.php
@@ -0,0 +1,69 @@
+<?php
+
+final class PhabricatorProjectTriggerUsageIndexEngineExtension
+ extends PhabricatorIndexEngineExtension {
+
+ const EXTENSIONKEY = 'trigger.usage';
+
+ public function getExtensionName() {
+ return pht('Trigger Usage');
+ }
+
+ public function shouldIndexObject($object) {
+ if (!($object instanceof PhabricatorProjectTrigger)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function indexObject(
+ PhabricatorIndexEngine $engine,
+ $object) {
+
+ $usage_table = new PhabricatorProjectTriggerUsage();
+ $column_table = new PhabricatorProjectColumn();
+
+ $conn_w = $object->establishConnection('w');
+
+ $active_statuses = array(
+ PhabricatorProjectColumn::STATUS_ACTIVE,
+ );
+
+ // Select summary information to populate the usage index. When picking
+ // an "examplePHID", we try to pick an active column.
+ $row = queryfx_one(
+ $conn_w,
+ 'SELECT phid, COUNT(*) N, SUM(IF(status IN (%Ls), 1, 0)) M FROM %R
+ WHERE triggerPHID = %s
+ ORDER BY IF(status IN (%Ls), 1, 0) DESC, id ASC',
+ $active_statuses,
+ $column_table,
+ $object->getPHID(),
+ $active_statuses);
+ if ($row) {
+ $example_phid = $row['phid'];
+ $column_count = $row['N'];
+ $active_count = $row['M'];
+ } else {
+ $example_phid = null;
+ $column_count = 0;
+ $active_count = 0;
+ }
+
+ queryfx(
+ $conn_w,
+ 'INSERT INTO %R (triggerPHID, examplePHID, columnCount, activeColumnCount)
+ VALUES (%s, %ns, %d, %d)
+ ON DUPLICATE KEY UPDATE
+ examplePHID = VALUES(examplePHID),
+ columnCount = VALUES(columnCount),
+ activeColumnCount = VALUES(activeColumnCount)',
+ $usage_table,
+ $object->getPHID(),
+ $example_phid,
+ $column_count,
+ $active_count);
+ }
+
+}
diff --git a/src/applications/project/engineextension/PhabricatorProjectsCurtainExtension.php b/src/applications/project/engineextension/PhabricatorProjectsCurtainExtension.php
index c69e13027..725132341 100644
--- a/src/applications/project/engineextension/PhabricatorProjectsCurtainExtension.php
+++ b/src/applications/project/engineextension/PhabricatorProjectsCurtainExtension.php
@@ -1,91 +1,91 @@
<?php
final class PhabricatorProjectsCurtainExtension
extends PHUICurtainExtension {
const EXTENSIONKEY = 'projects.projects';
public function shouldEnableForObject($object) {
return ($object instanceof PhabricatorProjectInterface);
}
public function getExtensionApplication() {
return new PhabricatorProjectApplication();
}
public function buildCurtainPanel($object) {
$viewer = $this->getViewer();
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
$has_projects = (bool)$project_phids;
$project_phids = array_reverse($project_phids);
$handles = $viewer->loadHandles($project_phids);
// If this object can appear on boards, build the workboard annotations.
// Some day, this might be a generic interface. For now, only tasks can
// appear on boards.
$can_appear_on_boards = ($object instanceof ManiphestTask);
$annotations = array();
if ($has_projects && $can_appear_on_boards) {
$engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs($project_phids)
->setObjectPHIDs(array($object->getPHID()))
->executeLayout();
// TDOO: Generalize this UI and move it out of Maniphest.
require_celerity_resource('maniphest-task-summary-css');
foreach ($project_phids as $project_phid) {
$handle = $handles[$project_phid];
$columns = $engine->getObjectColumns(
$project_phid,
$object->getPHID());
$annotation = array();
foreach ($columns as $column) {
$project_id = $column->getProject()->getID();
$column_name = pht('(%s)', $column->getDisplayName());
$column_link = phutil_tag(
'a',
array(
- 'href' => "/project/board/{$project_id}/",
+ 'href' => $column->getWorkboardURI(),
'class' => 'maniphest-board-link',
),
$column_name);
$annotation[] = $column_link;
}
if ($annotation) {
$annotations[$project_phid] = array(
' ',
phutil_implode_html(', ', $annotation),
);
}
}
}
if ($has_projects) {
$list = id(new PHUIHandleTagListView())
->setHandles($handles)
->setAnnotations($annotations)
->setShowHovercards(true);
} else {
$list = phutil_tag('em', array(), pht('None'));
}
return $this->newPanel()
->setHeaderText(pht('Tags'))
->setOrder(10000)
->appendChild($list);
}
}
diff --git a/src/applications/project/events/PhabricatorProjectUIEventListener.php b/src/applications/project/events/PhabricatorProjectUIEventListener.php
index 104084bbf..25d1ba9f7 100644
--- a/src/applications/project/events/PhabricatorProjectUIEventListener.php
+++ b/src/applications/project/events/PhabricatorProjectUIEventListener.php
@@ -1,115 +1,115 @@
<?php
final class PhabricatorProjectUIEventListener
extends PhabricatorEventListener {
public function register() {
$this->listen(PhabricatorEventType::TYPE_UI_WILLRENDERPROPERTIES);
}
public function handleEvent(PhutilEvent $event) {
$object = $event->getValue('object');
switch ($event->getType()) {
case PhabricatorEventType::TYPE_UI_WILLRENDERPROPERTIES:
// Hacky solution so that property list view on Diffusion
// commits shows build status, but not Projects, Subscriptions,
// or Tokens.
if ($object instanceof PhabricatorRepositoryCommit) {
return;
}
$this->handlePropertyEvent($event);
break;
}
}
private function handlePropertyEvent($event) {
$user = $event->getUser();
$object = $event->getValue('object');
if (!$object || !$object->getPHID()) {
// No object, or the object has no PHID yet..
return;
}
if (!($object instanceof PhabricatorProjectInterface)) {
// This object doesn't have projects.
return;
}
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
if ($project_phids) {
$project_phids = array_reverse($project_phids);
$handles = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs($project_phids)
->execute();
} else {
$handles = array();
}
// If this object can appear on boards, build the workboard annotations.
// Some day, this might be a generic interface. For now, only tasks can
// appear on boards.
$can_appear_on_boards = ($object instanceof ManiphestTask);
$annotations = array();
if ($handles && $can_appear_on_boards) {
$engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($user)
->setBoardPHIDs($project_phids)
->setObjectPHIDs(array($object->getPHID()))
->executeLayout();
// TDOO: Generalize this UI and move it out of Maniphest.
require_celerity_resource('maniphest-task-summary-css');
foreach ($project_phids as $project_phid) {
$handle = $handles[$project_phid];
$columns = $engine->getObjectColumns(
$project_phid,
$object->getPHID());
$annotation = array();
foreach ($columns as $column) {
$project_id = $column->getProject()->getID();
$column_name = pht('(%s)', $column->getDisplayName());
$column_link = phutil_tag(
'a',
array(
- 'href' => "/project/board/{$project_id}/",
+ 'href' => $column->getWorkboardURI(),
'class' => 'maniphest-board-link',
),
$column_name);
$annotation[] = $column_link;
}
if ($annotation) {
$annotations[$project_phid] = array(
' ',
phutil_implode_html(', ', $annotation),
);
}
}
}
if ($handles) {
$list = id(new PHUIHandleTagListView())
->setHandles($handles)
->setAnnotations($annotations)
->setShowHovercards(true);
} else {
$list = phutil_tag('em', array(), pht('None'));
}
$view = $event->getValue('view');
$view->addProperty(pht('Projects'), $list);
}
}
diff --git a/src/applications/project/exception/PhabricatorProjectTriggerCorruptionException.php b/src/applications/project/exception/PhabricatorProjectTriggerCorruptionException.php
new file mode 100644
index 000000000..c235fe735
--- /dev/null
+++ b/src/applications/project/exception/PhabricatorProjectTriggerCorruptionException.php
@@ -0,0 +1,4 @@
+<?php
+
+final class PhabricatorProjectTriggerCorruptionException
+ extends Exception {}
diff --git a/src/applications/project/icon/PhabricatorProjectDropEffect.php b/src/applications/project/icon/PhabricatorProjectDropEffect.php
new file mode 100644
index 000000000..3d61f9bce
--- /dev/null
+++ b/src/applications/project/icon/PhabricatorProjectDropEffect.php
@@ -0,0 +1,83 @@
+<?php
+
+final class PhabricatorProjectDropEffect
+ extends Phobject {
+
+ private $icon;
+ private $color;
+ private $content;
+ private $conditions = array();
+ private $isTriggerEffect;
+ private $isHeader;
+
+ public function setIcon($icon) {
+ $this->icon = $icon;
+ return $this;
+ }
+
+ public function getIcon() {
+ return $this->icon;
+ }
+
+ public function setColor($color) {
+ $this->color = $color;
+ return $this;
+ }
+
+ public function getColor() {
+ return $this->color;
+ }
+
+ public function setContent($content) {
+ $this->content = $content;
+ return $this;
+ }
+
+ public function getContent() {
+ return $this->content;
+ }
+
+ public function toDictionary() {
+ return array(
+ 'icon' => $this->getIcon(),
+ 'color' => $this->getColor(),
+ 'content' => hsprintf('%s', $this->getContent()),
+ 'isTriggerEffect' => $this->getIsTriggerEffect(),
+ 'isHeader' => $this->getIsHeader(),
+ 'conditions' => $this->getConditions(),
+ );
+ }
+
+ public function addCondition($field, $operator, $value) {
+ $this->conditions[] = array(
+ 'field' => $field,
+ 'operator' => $operator,
+ 'value' => $value,
+ );
+
+ return $this;
+ }
+
+ public function getConditions() {
+ return $this->conditions;
+ }
+
+ public function setIsTriggerEffect($is_trigger_effect) {
+ $this->isTriggerEffect = $is_trigger_effect;
+ return $this;
+ }
+
+ public function getIsTriggerEffect() {
+ return $this->isTriggerEffect;
+ }
+
+ public function setIsHeader($is_header) {
+ $this->isHeader = $is_header;
+ return $this;
+ }
+
+ public function getIsHeader() {
+ return $this->isHeader;
+ }
+
+}
diff --git a/src/applications/project/menuitem/PhabricatorProjectWorkboardProfileMenuItem.php b/src/applications/project/menuitem/PhabricatorProjectWorkboardProfileMenuItem.php
index 80ec0d835..38b9632d9 100644
--- a/src/applications/project/menuitem/PhabricatorProjectWorkboardProfileMenuItem.php
+++ b/src/applications/project/menuitem/PhabricatorProjectWorkboardProfileMenuItem.php
@@ -1,73 +1,73 @@
<?php
final class PhabricatorProjectWorkboardProfileMenuItem
extends PhabricatorProfileMenuItem {
const MENUITEMKEY = 'project.workboard';
public function getMenuItemTypeName() {
return pht('Project Workboard');
}
private function getDefaultName() {
return pht('Workboard');
}
public function canMakeDefault(
PhabricatorProfileMenuItemConfiguration $config) {
return true;
}
public function shouldEnableForObject($object) {
$viewer = $this->getViewer();
// Workboards are only available if Maniphest is installed.
$class = 'PhabricatorManiphestApplication';
if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) {
return false;
}
return true;
}
public function getDisplayName(
PhabricatorProfileMenuItemConfiguration $config) {
$name = $config->getMenuItemProperty('name');
if (strlen($name)) {
return $name;
}
return $this->getDefaultName();
}
public function buildEditEngineFields(
PhabricatorProfileMenuItemConfiguration $config) {
return array(
id(new PhabricatorTextEditField())
->setKey('name')
->setLabel(pht('Name'))
->setPlaceholder($this->getDefaultName())
->setValue($config->getMenuItemProperty('name')),
);
}
protected function newNavigationMenuItems(
PhabricatorProfileMenuItemConfiguration $config) {
$project = $config->getProfileObject();
$id = $project->getID();
- $href = "/project/board/{$id}/";
+ $href = $project->getWorkboardURI();
$name = $this->getDisplayName($config);
$item = $this->newItem()
->setHref($href)
->setName($name)
->setIcon('fa-columns');
return array(
$item,
);
}
}
diff --git a/src/applications/project/order/PhabricatorProjectColumnAuthorOrder.php b/src/applications/project/order/PhabricatorProjectColumnAuthorOrder.php
new file mode 100644
index 000000000..9d6bac2af
--- /dev/null
+++ b/src/applications/project/order/PhabricatorProjectColumnAuthorOrder.php
@@ -0,0 +1,139 @@
+<?php
+
+final class PhabricatorProjectColumnAuthorOrder
+ extends PhabricatorProjectColumnOrder {
+
+ const ORDERKEY = 'author';
+
+ public function getDisplayName() {
+ return pht('Group by Author');
+ }
+
+ protected function newMenuIconIcon() {
+ return 'fa-user-plus';
+ }
+
+ public function getHasHeaders() {
+ return true;
+ }
+
+ public function getCanReorder() {
+ return false;
+ }
+
+ public function getMenuOrder() {
+ return 3000;
+ }
+
+ protected function newHeaderKeyForObject($object) {
+ return $this->newHeaderKeyForAuthorPHID($object->getAuthorPHID());
+ }
+
+ private function newHeaderKeyForAuthorPHID($author_phid) {
+ return sprintf('author(%s)', $author_phid);
+ }
+
+ protected function newSortVectorsForObjects(array $objects) {
+ $author_phids = mpull($objects, null, 'getAuthorPHID');
+ $author_phids = array_keys($author_phids);
+ $author_phids = array_filter($author_phids);
+
+ if ($author_phids) {
+ $author_users = id(new PhabricatorPeopleQuery())
+ ->setViewer($this->getViewer())
+ ->withPHIDs($author_phids)
+ ->execute();
+ $author_users = mpull($author_users, null, 'getPHID');
+ } else {
+ $author_users = array();
+ }
+
+ $vectors = array();
+ foreach ($objects as $vector_key => $object) {
+ $author_phid = $object->getAuthorPHID();
+ $author = idx($author_users, $author_phid);
+ if ($author) {
+ $vector = $this->newSortVectorForAuthor($author);
+ } else {
+ $vector = $this->newSortVectorForAuthorPHID($author_phid);
+ }
+
+ $vectors[$vector_key] = $vector;
+ }
+
+ return $vectors;
+ }
+
+ private function newSortVectorForAuthor(PhabricatorUser $user) {
+ return array(
+ 1,
+ $user->getUsername(),
+ );
+ }
+
+ private function newSortVectorForAuthorPHID($author_phid) {
+ return array(
+ 2,
+ $author_phid,
+ );
+ }
+
+ protected function newHeadersForObjects(array $objects) {
+ $author_phids = mpull($objects, null, 'getAuthorPHID');
+ $author_phids = array_keys($author_phids);
+ $author_phids = array_filter($author_phids);
+
+ if ($author_phids) {
+ $author_users = id(new PhabricatorPeopleQuery())
+ ->setViewer($this->getViewer())
+ ->withPHIDs($author_phids)
+ ->needProfileImage(true)
+ ->execute();
+ $author_users = mpull($author_users, null, 'getPHID');
+ } else {
+ $author_users = array();
+ }
+
+ $headers = array();
+ foreach ($author_phids as $author_phid) {
+ $header_key = $this->newHeaderKeyForAuthorPHID($author_phid);
+
+ $author = idx($author_users, $author_phid);
+ if ($author) {
+ $sort_vector = $this->newSortVectorForAuthor($author);
+ $author_name = $author->getUsername();
+ $author_image = $author->getProfileImageURI();
+ } else {
+ $sort_vector = $this->newSortVectorForAuthorPHID($author_phid);
+ $author_name = pht('Unknown User ("%s")', $author_phid);
+ $author_image = null;
+ }
+
+ $author_icon = 'fa-user';
+ $author_color = 'bluegrey';
+
+ $icon_view = id(new PHUIIconView());
+
+ if ($author_image) {
+ $icon_view->setImage($author_image);
+ } else {
+ $icon_view->setIcon($author_icon, $author_color);
+ }
+
+ $header = $this->newHeader()
+ ->setHeaderKey($header_key)
+ ->setSortVector($sort_vector)
+ ->setName($author_name)
+ ->setIcon($icon_view)
+ ->setEditProperties(
+ array(
+ 'value' => $author_phid,
+ ));
+
+ $headers[] = $header;
+ }
+
+ return $headers;
+ }
+
+}
diff --git a/src/applications/project/order/PhabricatorProjectColumnCreatedOrder.php b/src/applications/project/order/PhabricatorProjectColumnCreatedOrder.php
new file mode 100644
index 000000000..05f25a3d6
--- /dev/null
+++ b/src/applications/project/order/PhabricatorProjectColumnCreatedOrder.php
@@ -0,0 +1,35 @@
+<?php
+
+final class PhabricatorProjectColumnCreatedOrder
+ extends PhabricatorProjectColumnOrder {
+
+ const ORDERKEY = 'created';
+
+ public function getDisplayName() {
+ return pht('Sort by Created Date');
+ }
+
+ protected function newMenuIconIcon() {
+ return 'fa-clock-o';
+ }
+
+ public function getHasHeaders() {
+ return false;
+ }
+
+ public function getCanReorder() {
+ return false;
+ }
+
+ public function getMenuOrder() {
+ return 5000;
+ }
+
+ protected function newSortVectorForObject($object) {
+ return array(
+ -1 * (int)$object->getDateCreated(),
+ -1 * (int)$object->getID(),
+ );
+ }
+
+}
diff --git a/src/applications/project/order/PhabricatorProjectColumnHeader.php b/src/applications/project/order/PhabricatorProjectColumnHeader.php
new file mode 100644
index 000000000..898d9b022
--- /dev/null
+++ b/src/applications/project/order/PhabricatorProjectColumnHeader.php
@@ -0,0 +1,110 @@
+<?php
+
+final class PhabricatorProjectColumnHeader
+ extends Phobject {
+
+ private $orderKey;
+ private $headerKey;
+ private $sortVector;
+ private $name;
+ private $icon;
+ private $editProperties;
+ private $dropEffects = array();
+
+ public function setOrderKey($order_key) {
+ $this->orderKey = $order_key;
+ return $this;
+ }
+
+ public function getOrderKey() {
+ return $this->orderKey;
+ }
+
+ public function setHeaderKey($header_key) {
+ $this->headerKey = $header_key;
+ return $this;
+ }
+
+ public function getHeaderKey() {
+ return $this->headerKey;
+ }
+
+ public function setSortVector(array $sort_vector) {
+ $this->sortVector = $sort_vector;
+ return $this;
+ }
+
+ public function getSortVector() {
+ return $this->sortVector;
+ }
+
+ public function setName($name) {
+ $this->name = $name;
+ return $this;
+ }
+
+ public function getName() {
+ return $this->name;
+ }
+
+ public function setIcon(PHUIIconView$icon) {
+ $this->icon = $icon;
+ return $this;
+ }
+
+ public function getIcon() {
+ return $this->icon;
+ }
+
+ public function setEditProperties(array $edit_properties) {
+ $this->editProperties = $edit_properties;
+ return $this;
+ }
+
+ public function getEditProperties() {
+ return $this->editProperties;
+ }
+
+ public function addDropEffect(PhabricatorProjectDropEffect $effect) {
+ $this->dropEffects[] = $effect;
+ return $this;
+ }
+
+ public function getDropEffects() {
+ return $this->dropEffects;
+ }
+
+ public function toDictionary() {
+ return array(
+ 'order' => $this->getOrderKey(),
+ 'key' => $this->getHeaderKey(),
+ 'template' => hsprintf('%s', $this->newView()),
+ 'vector' => $this->getSortVector(),
+ 'editProperties' => $this->getEditProperties(),
+ 'effects' => mpull($this->getDropEffects(), 'toDictionary'),
+ );
+ }
+
+ private function newView() {
+ $icon_view = $this->getIcon();
+ $name = $this->getName();
+
+ $template = phutil_tag(
+ 'li',
+ array(
+ 'class' => 'workboard-group-header',
+ ),
+ array(
+ $icon_view,
+ phutil_tag(
+ 'span',
+ array(
+ 'class' => 'workboard-group-header-name',
+ ),
+ $name),
+ ));
+
+ return $template;
+ }
+
+}
diff --git a/src/applications/project/order/PhabricatorProjectColumnNaturalOrder.php b/src/applications/project/order/PhabricatorProjectColumnNaturalOrder.php
new file mode 100644
index 000000000..be67d28bc
--- /dev/null
+++ b/src/applications/project/order/PhabricatorProjectColumnNaturalOrder.php
@@ -0,0 +1,24 @@
+<?php
+
+final class PhabricatorProjectColumnNaturalOrder
+ extends PhabricatorProjectColumnOrder {
+
+ const ORDERKEY = 'natural';
+
+ public function getDisplayName() {
+ return pht('Natural');
+ }
+
+ public function getHasHeaders() {
+ return false;
+ }
+
+ public function getCanReorder() {
+ return true;
+ }
+
+ public function getMenuOrder() {
+ return 0;
+ }
+
+}
diff --git a/src/applications/project/order/PhabricatorProjectColumnOrder.php b/src/applications/project/order/PhabricatorProjectColumnOrder.php
new file mode 100644
index 000000000..430d9ef47
--- /dev/null
+++ b/src/applications/project/order/PhabricatorProjectColumnOrder.php
@@ -0,0 +1,211 @@
+<?php
+
+abstract class PhabricatorProjectColumnOrder
+ extends Phobject {
+
+ private $viewer;
+
+ final public function setViewer(PhabricatorUser $viewer) {
+ $this->viewer = $viewer;
+ return $this;
+ }
+
+ final public function getViewer() {
+ return $this->viewer;
+ }
+
+ final public function getColumnOrderKey() {
+ return $this->getPhobjectClassConstant('ORDERKEY');
+ }
+
+ final public static function getAllOrders() {
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getColumnOrderKey')
+ ->setSortMethod('getMenuOrder')
+ ->execute();
+ }
+
+ final public static function getEnabledOrders() {
+ $map = self::getAllOrders();
+
+ foreach ($map as $key => $order) {
+ if (!$order->isEnabled()) {
+ unset($map[$key]);
+ }
+ }
+
+ return $map;
+ }
+
+ final public static function getOrderByKey($key) {
+ $map = self::getAllOrders();
+
+ if (!isset($map[$key])) {
+ throw new Exception(
+ pht(
+ 'No column ordering exists with key "%s".',
+ $key));
+ }
+
+ return $map[$key];
+ }
+
+ final public function getColumnTransactions($object, array $header) {
+ $result = $this->newColumnTransactions($object, $header);
+
+ if (!is_array($result) && !is_null($result)) {
+ throw new Exception(
+ pht(
+ 'Expected "newColumnTransactions()" on "%s" to return "null" or a '.
+ 'list of transactions, but got "%s".',
+ get_class($this),
+ phutil_describe_type($result)));
+ }
+
+ if ($result === null) {
+ $result = array();
+ }
+
+ assert_instances_of($result, 'PhabricatorApplicationTransaction');
+
+ return $result;
+ }
+
+ final public function getMenuIconIcon() {
+ return $this->newMenuIconIcon();
+ }
+
+ protected function newMenuIconIcon() {
+ return 'fa-sort-amount-asc';
+ }
+
+ abstract public function getDisplayName();
+ abstract public function getHasHeaders();
+ abstract public function getCanReorder();
+
+ public function getMenuOrder() {
+ return 9000;
+ }
+
+ public function isEnabled() {
+ return true;
+ }
+
+ protected function newColumnTransactions($object, array $header) {
+ return array();
+ }
+
+ final public function getHeadersForObjects(array $objects) {
+ $headers = $this->newHeadersForObjects($objects);
+
+ if (!is_array($headers)) {
+ throw new Exception(
+ pht(
+ 'Expected "newHeadersForObjects()" on "%s" to return a list '.
+ 'of headers, but got "%s".',
+ get_class($this),
+ phutil_describe_type($headers)));
+ }
+
+ assert_instances_of($headers, 'PhabricatorProjectColumnHeader');
+
+ // Add a "0" to the end of each header. This makes them sort above object
+ // cards in the same group.
+ foreach ($headers as $header) {
+ $vector = $header->getSortVector();
+ $vector[] = 0;
+ $header->setSortVector($vector);
+ }
+
+ return $headers;
+ }
+
+ protected function newHeadersForObjects(array $objects) {
+ return array();
+ }
+
+ final public function getSortVectorsForObjects(array $objects) {
+ $vectors = $this->newSortVectorsForObjects($objects);
+
+ if (!is_array($vectors)) {
+ throw new Exception(
+ pht(
+ 'Expected "newSortVectorsForObjects()" on "%s" to return a '.
+ 'map of vectors, but got "%s".',
+ get_class($this),
+ phutil_describe_type($vectors)));
+ }
+
+ assert_same_keys($objects, $vectors);
+
+ return $vectors;
+ }
+
+ protected function newSortVectorsForObjects(array $objects) {
+ $vectors = array();
+
+ foreach ($objects as $key => $object) {
+ $vectors[$key] = $this->newSortVectorForObject($object);
+ }
+
+ return $vectors;
+ }
+
+ protected function newSortVectorForObject($object) {
+ return array();
+ }
+
+ final public function getHeaderKeysForObjects(array $objects) {
+ $header_keys = $this->newHeaderKeysForObjects($objects);
+
+ if (!is_array($header_keys)) {
+ throw new Exception(
+ pht(
+ 'Expected "newHeaderKeysForObject()" on "%s" to return a '.
+ 'map of header keys, but got "%s".',
+ get_class($this),
+ phutil_describe_type($header_keys)));
+ }
+
+ assert_same_keys($objects, $header_keys);
+
+ return $header_keys;
+ }
+
+ protected function newHeaderKeysForObjects(array $objects) {
+ $header_keys = array();
+
+ foreach ($objects as $key => $object) {
+ $header_keys[$key] = $this->newHeaderKeyForObject($object);
+ }
+
+ return $header_keys;
+ }
+
+ protected function newHeaderKeyForObject($object) {
+ return null;
+ }
+
+ final protected function newTransaction($object) {
+ return $object->getApplicationTransactionTemplate();
+ }
+
+ final protected function newHeader() {
+ return id(new PhabricatorProjectColumnHeader())
+ ->setOrderKey($this->getColumnOrderKey());
+ }
+
+ final protected function newEffect() {
+ return new PhabricatorProjectDropEffect();
+ }
+
+ final public function toDictionary() {
+ return array(
+ 'orderKey' => $this->getColumnOrderKey(),
+ 'hasHeaders' => $this->getHasHeaders(),
+ 'canReorder' => $this->getCanReorder(),
+ );
+ }
+
+}
diff --git a/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php b/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php
new file mode 100644
index 000000000..48a6c394d
--- /dev/null
+++ b/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php
@@ -0,0 +1,199 @@
+<?php
+
+final class PhabricatorProjectColumnOwnerOrder
+ extends PhabricatorProjectColumnOrder {
+
+ const ORDERKEY = 'owner';
+
+ public function getDisplayName() {
+ return pht('Group by Owner');
+ }
+
+ protected function newMenuIconIcon() {
+ return 'fa-users';
+ }
+
+ public function getHasHeaders() {
+ return true;
+ }
+
+ public function getCanReorder() {
+ return true;
+ }
+
+ public function getMenuOrder() {
+ return 2000;
+ }
+
+ protected function newHeaderKeyForObject($object) {
+ return $this->newHeaderKeyForOwnerPHID($object->getOwnerPHID());
+ }
+
+ private function newHeaderKeyForOwnerPHID($owner_phid) {
+ if ($owner_phid === null) {
+ $owner_phid = '<null>';
+ }
+
+ return sprintf('owner(%s)', $owner_phid);
+ }
+
+ protected function newSortVectorsForObjects(array $objects) {
+ $owner_phids = mpull($objects, null, 'getOwnerPHID');
+ $owner_phids = array_keys($owner_phids);
+ $owner_phids = array_filter($owner_phids);
+
+ if ($owner_phids) {
+ $owner_users = id(new PhabricatorPeopleQuery())
+ ->setViewer($this->getViewer())
+ ->withPHIDs($owner_phids)
+ ->execute();
+ $owner_users = mpull($owner_users, null, 'getPHID');
+ } else {
+ $owner_users = array();
+ }
+
+ $vectors = array();
+ foreach ($objects as $vector_key => $object) {
+ $owner_phid = $object->getOwnerPHID();
+ if (!$owner_phid) {
+ $vector = $this->newSortVectorForUnowned();
+ } else {
+ $owner = idx($owner_users, $owner_phid);
+ if ($owner) {
+ $vector = $this->newSortVectorForOwner($owner);
+ } else {
+ $vector = $this->newSortVectorForOwnerPHID($owner_phid);
+ }
+ }
+
+ $vectors[$vector_key] = $vector;
+ }
+
+ return $vectors;
+ }
+
+ private function newSortVectorForUnowned() {
+ // Always put unasssigned tasks at the top.
+ return array(
+ 0,
+ );
+ }
+
+ private function newSortVectorForOwner(PhabricatorUser $user) {
+ // Put assigned tasks with a valid owner after "Unassigned", but above
+ // assigned tasks with an invalid owner. Sort these tasks by the owner's
+ // username.
+ return array(
+ 1,
+ $user->getUsername(),
+ );
+ }
+
+ private function newSortVectorForOwnerPHID($owner_phid) {
+ // If we have tasks with a nonempty owner but can't load the associated
+ // "User" object, move them to the bottom. We can only sort these by the
+ // PHID.
+ return array(
+ 2,
+ $owner_phid,
+ );
+ }
+
+ protected function newHeadersForObjects(array $objects) {
+ $owner_phids = mpull($objects, null, 'getOwnerPHID');
+ $owner_phids = array_keys($owner_phids);
+ $owner_phids = array_filter($owner_phids);
+
+ if ($owner_phids) {
+ $owner_users = id(new PhabricatorPeopleQuery())
+ ->setViewer($this->getViewer())
+ ->withPHIDs($owner_phids)
+ ->needProfileImage(true)
+ ->execute();
+ $owner_users = mpull($owner_users, null, 'getPHID');
+ } else {
+ $owner_users = array();
+ }
+
+ array_unshift($owner_phids, null);
+
+ $headers = array();
+ foreach ($owner_phids as $owner_phid) {
+ $header_key = $this->newHeaderKeyForOwnerPHID($owner_phid);
+
+ $owner_image = null;
+ $effect_content = null;
+ if ($owner_phid === null) {
+ $owner = null;
+ $sort_vector = $this->newSortVectorForUnowned();
+ $owner_name = pht('Not Assigned');
+
+ $effect_content = pht('Remove task assignee.');
+ } else {
+ $owner = idx($owner_users, $owner_phid);
+ if ($owner) {
+ $sort_vector = $this->newSortVectorForOwner($owner);
+ $owner_name = $owner->getUsername();
+ $owner_image = $owner->getProfileImageURI();
+
+ $effect_content = pht(
+ 'Assign task to %s.',
+ phutil_tag('strong', array(), $owner_name));
+ } else {
+ $sort_vector = $this->newSortVectorForOwnerPHID($owner_phid);
+ $owner_name = pht('Unknown User ("%s")', $owner_phid);
+ }
+ }
+
+ $owner_icon = 'fa-user';
+ $owner_color = 'bluegrey';
+
+ $icon_view = id(new PHUIIconView());
+
+ if ($owner_image) {
+ $icon_view->setImage($owner_image);
+ } else {
+ $icon_view->setIcon($owner_icon, $owner_color);
+ }
+
+ $header = $this->newHeader()
+ ->setHeaderKey($header_key)
+ ->setSortVector($sort_vector)
+ ->setName($owner_name)
+ ->setIcon($icon_view)
+ ->setEditProperties(
+ array(
+ 'value' => $owner_phid,
+ ));
+
+ if ($effect_content !== null) {
+ $header->addDropEffect(
+ $this->newEffect()
+ ->setIcon($owner_icon)
+ ->setColor($owner_color)
+ ->addCondition('owner', '!=', $owner_phid)
+ ->setContent($effect_content));
+ }
+
+ $headers[] = $header;
+ }
+
+ return $headers;
+ }
+
+ protected function newColumnTransactions($object, array $header) {
+ $new_owner = idx($header, 'value');
+
+ if ($object->getOwnerPHID() === $new_owner) {
+ return null;
+ }
+
+ $xactions = array();
+ $xactions[] = $this->newTransaction($object)
+ ->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE)
+ ->setNewValue($new_owner);
+
+ return $xactions;
+ }
+
+}
diff --git a/src/applications/project/order/PhabricatorProjectColumnPointsOrder.php b/src/applications/project/order/PhabricatorProjectColumnPointsOrder.php
new file mode 100644
index 000000000..2e9be8e4b
--- /dev/null
+++ b/src/applications/project/order/PhabricatorProjectColumnPointsOrder.php
@@ -0,0 +1,50 @@
+<?php
+
+final class PhabricatorProjectColumnPointsOrder
+ extends PhabricatorProjectColumnOrder {
+
+ const ORDERKEY = 'points';
+
+ public function getDisplayName() {
+ return pht('Sort by Points');
+ }
+
+ protected function newMenuIconIcon() {
+ return 'fa-map-pin';
+ }
+
+ public function isEnabled() {
+ return ManiphestTaskPoints::getIsEnabled();
+ }
+
+ public function getHasHeaders() {
+ return false;
+ }
+
+ public function getCanReorder() {
+ return false;
+ }
+
+ public function getMenuOrder() {
+ return 6000;
+ }
+
+ protected function newSortVectorForObject($object) {
+ $points = $object->getPoints();
+
+ // Put cards with no points on top.
+ $has_points = ($points !== null);
+ if (!$has_points) {
+ $overall_order = 0;
+ } else {
+ $overall_order = 1;
+ }
+
+ return array(
+ $overall_order,
+ -1.0 * (double)$points,
+ -1 * (int)$object->getID(),
+ );
+ }
+
+}
diff --git a/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php b/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php
new file mode 100644
index 000000000..42ccf9655
--- /dev/null
+++ b/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php
@@ -0,0 +1,113 @@
+<?php
+
+final class PhabricatorProjectColumnPriorityOrder
+ extends PhabricatorProjectColumnOrder {
+
+ const ORDERKEY = 'priority';
+
+ public function getDisplayName() {
+ return pht('Group by Priority');
+ }
+
+ protected function newMenuIconIcon() {
+ return 'fa-sort-numeric-asc';
+ }
+
+ public function getHasHeaders() {
+ return true;
+ }
+
+ public function getCanReorder() {
+ return true;
+ }
+
+ public function getMenuOrder() {
+ return 1000;
+ }
+
+ protected function newHeaderKeyForObject($object) {
+ return $this->newHeaderKeyForPriority($object->getPriority());
+ }
+
+ private function newHeaderKeyForPriority($priority) {
+ return sprintf('priority(%d)', $priority);
+ }
+
+ protected function newSortVectorForObject($object) {
+ return $this->newSortVectorForPriority($object->getPriority());
+ }
+
+ private function newSortVectorForPriority($priority) {
+ return array(
+ -1 * (int)$priority,
+ );
+ }
+
+ protected function newHeadersForObjects(array $objects) {
+ $priorities = ManiphestTaskPriority::getTaskPriorityMap();
+
+ // It's possible for tasks to have an invalid/unknown priority in the
+ // database. We still want to generate a header for these tasks so we
+ // don't break the workboard.
+ $priorities = $priorities + mpull($objects, null, 'getPriority');
+
+ $priorities = array_keys($priorities);
+
+ $headers = array();
+ foreach ($priorities as $priority) {
+ $header_key = $this->newHeaderKeyForPriority($priority);
+ $sort_vector = $this->newSortVectorForPriority($priority);
+
+ $priority_name = ManiphestTaskPriority::getTaskPriorityName($priority);
+ $priority_color = ManiphestTaskPriority::getTaskPriorityColor($priority);
+ $priority_icon = ManiphestTaskPriority::getTaskPriorityIcon($priority);
+
+ $icon_view = id(new PHUIIconView())
+ ->setIcon($priority_icon, $priority_color);
+
+ $drop_effect = $this->newEffect()
+ ->setIcon($priority_icon)
+ ->setColor($priority_color)
+ ->addCondition('priority', '!=', $priority)
+ ->setContent(
+ pht(
+ 'Change priority to %s.',
+ phutil_tag('strong', array(), $priority_name)));
+
+ $header = $this->newHeader()
+ ->setHeaderKey($header_key)
+ ->setSortVector($sort_vector)
+ ->setName($priority_name)
+ ->setIcon($icon_view)
+ ->setEditProperties(
+ array(
+ 'value' => (int)$priority,
+ ))
+ ->addDropEffect($drop_effect);
+
+ $headers[] = $header;
+ }
+
+ return $headers;
+ }
+
+ protected function newColumnTransactions($object, array $header) {
+ $new_priority = idx($header, 'value');
+
+ if ($object->getPriority() === $new_priority) {
+ return null;
+ }
+
+ $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap();
+ $keyword = head(idx($keyword_map, $new_priority));
+
+ $xactions = array();
+ $xactions[] = $this->newTransaction($object)
+ ->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE)
+ ->setNewValue($keyword);
+
+ return $xactions;
+ }
+
+
+}
diff --git a/src/applications/project/order/PhabricatorProjectColumnStatusOrder.php b/src/applications/project/order/PhabricatorProjectColumnStatusOrder.php
new file mode 100644
index 000000000..2cb156aa9
--- /dev/null
+++ b/src/applications/project/order/PhabricatorProjectColumnStatusOrder.php
@@ -0,0 +1,116 @@
+<?php
+
+final class PhabricatorProjectColumnStatusOrder
+ extends PhabricatorProjectColumnOrder {
+
+ const ORDERKEY = 'status';
+
+ public function getDisplayName() {
+ return pht('Group by Status');
+ }
+
+ protected function newMenuIconIcon() {
+ return 'fa-check';
+ }
+
+ public function getHasHeaders() {
+ return true;
+ }
+
+ public function getCanReorder() {
+ return true;
+ }
+
+ public function getMenuOrder() {
+ return 4000;
+ }
+
+ protected function newHeaderKeyForObject($object) {
+ return $this->newHeaderKeyForStatus($object->getStatus());
+ }
+
+ private function newHeaderKeyForStatus($status) {
+ return sprintf('status(%s)', $status);
+ }
+
+ protected function newSortVectorsForObjects(array $objects) {
+ $status_sequence = $this->newStatusSequence();
+
+ $vectors = array();
+ foreach ($objects as $object_key => $object) {
+ $vectors[$object_key] = array(
+ (int)idx($status_sequence, $object->getStatus(), 0),
+ );
+ }
+
+ return $vectors;
+ }
+
+ private function newStatusSequence() {
+ $statuses = ManiphestTaskStatus::getTaskStatusMap();
+ return array_combine(
+ array_keys($statuses),
+ range(1, count($statuses)));
+ }
+
+ protected function newHeadersForObjects(array $objects) {
+ $headers = array();
+
+ $statuses = ManiphestTaskStatus::getTaskStatusMap();
+ $sequence = $this->newStatusSequence();
+
+ foreach ($statuses as $status_key => $status_name) {
+ $header_key = $this->newHeaderKeyForStatus($status_key);
+
+ $sort_vector = array(
+ (int)idx($sequence, $status_key, 0),
+ );
+
+ $status_icon = ManiphestTaskStatus::getStatusIcon($status_key);
+ $status_color = ManiphestTaskStatus::getStatusColor($status_key);
+
+ $icon_view = id(new PHUIIconView())
+ ->setIcon($status_icon, $status_color);
+
+ $drop_effect = $this->newEffect()
+ ->setIcon($status_icon)
+ ->setColor($status_color)
+ ->addCondition('status', '!=', $status_key)
+ ->setContent(
+ pht(
+ 'Change status to %s.',
+ phutil_tag('strong', array(), $status_name)));
+
+ $header = $this->newHeader()
+ ->setHeaderKey($header_key)
+ ->setSortVector($sort_vector)
+ ->setName($status_name)
+ ->setIcon($icon_view)
+ ->setEditProperties(
+ array(
+ 'value' => $status_key,
+ ))
+ ->addDropEffect($drop_effect);
+
+ $headers[] = $header;
+ }
+
+ return $headers;
+ }
+
+ protected function newColumnTransactions($object, array $header) {
+ $new_status = idx($header, 'value');
+
+ if ($object->getStatus() === $new_status) {
+ return null;
+ }
+
+ $xactions = array();
+ $xactions[] = $this->newTransaction($object)
+ ->setTransactionType(ManiphestTaskStatusTransaction::TRANSACTIONTYPE)
+ ->setNewValue($new_status);
+
+ return $xactions;
+ }
+
+}
diff --git a/src/applications/project/order/PhabricatorProjectColumnTitleOrder.php b/src/applications/project/order/PhabricatorProjectColumnTitleOrder.php
new file mode 100644
index 000000000..a281c7543
--- /dev/null
+++ b/src/applications/project/order/PhabricatorProjectColumnTitleOrder.php
@@ -0,0 +1,34 @@
+<?php
+
+final class PhabricatorProjectColumnTitleOrder
+ extends PhabricatorProjectColumnOrder {
+
+ const ORDERKEY = 'title';
+
+ public function getDisplayName() {
+ return pht('Sort by Title');
+ }
+
+ protected function newMenuIconIcon() {
+ return 'fa-sort-alpha-asc';
+ }
+
+ public function getHasHeaders() {
+ return false;
+ }
+
+ public function getCanReorder() {
+ return false;
+ }
+
+ public function getMenuOrder() {
+ return 7000;
+ }
+
+ protected function newSortVectorForObject($object) {
+ return array(
+ $object->getTitle(),
+ );
+ }
+
+}
diff --git a/src/applications/project/phid/PhabricatorProjectColumnPHIDType.php b/src/applications/project/phid/PhabricatorProjectColumnPHIDType.php
index 07c7f7a0e..c58bb4467 100644
--- a/src/applications/project/phid/PhabricatorProjectColumnPHIDType.php
+++ b/src/applications/project/phid/PhabricatorProjectColumnPHIDType.php
@@ -1,48 +1,48 @@
<?php
final class PhabricatorProjectColumnPHIDType extends PhabricatorPHIDType {
const TYPECONST = 'PCOL';
public function getTypeName() {
return pht('Project Column');
}
public function getTypeIcon() {
return 'fa-columns bluegrey';
}
public function newObject() {
return new PhabricatorProjectColumn();
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorProjectApplication';
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new PhabricatorProjectColumnQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$column = $objects[$phid];
$handle->setName($column->getDisplayName());
- $handle->setURI('/project/board/'.$column->getProject()->getID().'/');
+ $handle->setURI($column->getWorkboardURI());
if ($column->isHidden()) {
$handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED);
}
}
}
}
diff --git a/src/applications/project/phid/PhabricatorProjectTriggerPHIDType.php b/src/applications/project/phid/PhabricatorProjectTriggerPHIDType.php
new file mode 100644
index 000000000..346b0e69f
--- /dev/null
+++ b/src/applications/project/phid/PhabricatorProjectTriggerPHIDType.php
@@ -0,0 +1,45 @@
+<?php
+
+final class PhabricatorProjectTriggerPHIDType
+ extends PhabricatorPHIDType {
+
+ const TYPECONST = 'WTRG';
+
+ public function getTypeName() {
+ return pht('Trigger');
+ }
+
+ public function getTypeIcon() {
+ return 'fa-exclamation-triangle';
+ }
+
+ public function newObject() {
+ return new PhabricatorProjectTrigger();
+ }
+
+ public function getPHIDTypeApplicationClass() {
+ return 'PhabricatorProjectApplication';
+ }
+
+ protected function buildQueryForObjects(
+ PhabricatorObjectQuery $query,
+ array $phids) {
+
+ return id(new PhabricatorProjectTriggerQuery())
+ ->withPHIDs($phids);
+ }
+
+ public function loadHandles(
+ PhabricatorHandleQuery $query,
+ array $handles,
+ array $objects) {
+
+ foreach ($handles as $phid => $handle) {
+ $trigger = $objects[$phid];
+
+ $handle->setName($trigger->getDisplayName());
+ $handle->setURI($trigger->getURI());
+ }
+ }
+
+}
diff --git a/src/applications/project/query/PhabricatorProjectColumnQuery.php b/src/applications/project/query/PhabricatorProjectColumnQuery.php
index 441c33e8c..380dab520 100644
--- a/src/applications/project/query/PhabricatorProjectColumnQuery.php
+++ b/src/applications/project/query/PhabricatorProjectColumnQuery.php
@@ -1,180 +1,235 @@
<?php
final class PhabricatorProjectColumnQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $projectPHIDs;
private $proxyPHIDs;
private $statuses;
private $isProxyColumn;
+ private $triggerPHIDs;
+ private $needTriggers;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withProjectPHIDs(array $project_phids) {
$this->projectPHIDs = $project_phids;
return $this;
}
public function withProxyPHIDs(array $proxy_phids) {
$this->proxyPHIDs = $proxy_phids;
return $this;
}
public function withStatuses(array $status) {
$this->statuses = $status;
return $this;
}
public function withIsProxyColumn($is_proxy) {
$this->isProxyColumn = $is_proxy;
return $this;
}
+ public function withTriggerPHIDs(array $trigger_phids) {
+ $this->triggerPHIDs = $trigger_phids;
+ return $this;
+ }
+
+ public function needTriggers($need_triggers) {
+ $this->needTriggers = true;
+ return $this;
+ }
+
public function newResultObject() {
return new PhabricatorProjectColumn();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $page) {
$projects = array();
$project_phids = array_filter(mpull($page, 'getProjectPHID'));
if ($project_phids) {
$projects = id(new PhabricatorProjectQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($project_phids)
->execute();
$projects = mpull($projects, null, 'getPHID');
}
foreach ($page as $key => $column) {
$phid = $column->getProjectPHID();
$project = idx($projects, $phid);
if (!$project) {
$this->didRejectResult($page[$key]);
unset($page[$key]);
continue;
}
$column->attachProject($project);
}
$proxy_phids = array_filter(mpull($page, 'getProjectPHID'));
return $page;
}
protected function didFilterPage(array $page) {
$proxy_phids = array();
foreach ($page as $column) {
$proxy_phid = $column->getProxyPHID();
if ($proxy_phid !== null) {
$proxy_phids[$proxy_phid] = $proxy_phid;
}
}
if ($proxy_phids) {
$proxies = id(new PhabricatorObjectQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($proxy_phids)
->execute();
$proxies = mpull($proxies, null, 'getPHID');
} else {
$proxies = array();
}
foreach ($page as $key => $column) {
$proxy_phid = $column->getProxyPHID();
if ($proxy_phid !== null) {
$proxy = idx($proxies, $proxy_phid);
// Only attach valid proxies, so we don't end up getting surprised if
// an install somehow gets junk into their database.
if (!($proxy instanceof PhabricatorColumnProxyInterface)) {
$proxy = null;
}
if (!$proxy) {
$this->didRejectResult($column);
unset($page[$key]);
continue;
}
} else {
$proxy = null;
}
$column->attachProxy($proxy);
}
+ if ($this->needTriggers) {
+ $trigger_phids = array();
+ foreach ($page as $column) {
+ if ($column->canHaveTrigger()) {
+ $trigger_phid = $column->getTriggerPHID();
+ if ($trigger_phid) {
+ $trigger_phids[] = $trigger_phid;
+ }
+ }
+ }
+
+ if ($trigger_phids) {
+ $triggers = id(new PhabricatorProjectTriggerQuery())
+ ->setViewer($this->getViewer())
+ ->setParentQuery($this)
+ ->withPHIDs($trigger_phids)
+ ->execute();
+ $triggers = mpull($triggers, null, 'getPHID');
+ } else {
+ $triggers = array();
+ }
+
+ foreach ($page as $column) {
+ $trigger = null;
+
+ if ($column->canHaveTrigger()) {
+ $trigger_phid = $column->getTriggerPHID();
+ if ($trigger_phid) {
+ $trigger = idx($triggers, $trigger_phid);
+ }
+ }
+
+ $column->attachTrigger($trigger);
+ }
+ }
+
return $page;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->projectPHIDs !== null) {
$where[] = qsprintf(
$conn,
'projectPHID IN (%Ls)',
$this->projectPHIDs);
}
if ($this->proxyPHIDs !== null) {
$where[] = qsprintf(
$conn,
'proxyPHID IN (%Ls)',
$this->proxyPHIDs);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'status IN (%Ld)',
$this->statuses);
}
+ if ($this->triggerPHIDs !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'triggerPHID IN (%Ls)',
+ $this->triggerPHIDs);
+ }
+
if ($this->isProxyColumn !== null) {
if ($this->isProxyColumn) {
$where[] = qsprintf($conn, 'proxyPHID IS NOT NULL');
} else {
$where[] = qsprintf($conn, 'proxyPHID IS NULL');
}
}
return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorProjectApplication';
}
}
diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php
index f6087f7d2..b08a58501 100644
--- a/src/applications/project/query/PhabricatorProjectQuery.php
+++ b/src/applications/project/query/PhabricatorProjectQuery.php
@@ -1,878 +1,877 @@
<?php
final class PhabricatorProjectQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $memberPHIDs;
private $watcherPHIDs;
private $slugs;
private $slugNormals;
private $slugMap;
private $allSlugs;
private $names;
private $namePrefixes;
private $nameTokens;
private $icons;
private $colors;
private $ancestorPHIDs;
private $parentPHIDs;
private $isMilestone;
private $hasSubprojects;
private $minDepth;
private $maxDepth;
private $minMilestoneNumber;
private $maxMilestoneNumber;
private $subtypes;
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
const STATUS_CLOSED = 'status-closed';
const STATUS_ACTIVE = 'status-active';
const STATUS_ARCHIVED = 'status-archived';
private $statuses;
private $needSlugs;
private $needMembers;
private $needAncestorMembers;
private $needWatchers;
private $needImages;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withMemberPHIDs(array $member_phids) {
$this->memberPHIDs = $member_phids;
return $this;
}
public function withWatcherPHIDs(array $watcher_phids) {
$this->watcherPHIDs = $watcher_phids;
return $this;
}
public function withSlugs(array $slugs) {
$this->slugs = $slugs;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withNamePrefixes(array $prefixes) {
$this->namePrefixes = $prefixes;
return $this;
}
public function withNameTokens(array $tokens) {
$this->nameTokens = array_values($tokens);
return $this;
}
public function withIcons(array $icons) {
$this->icons = $icons;
return $this;
}
public function withColors(array $colors) {
$this->colors = $colors;
return $this;
}
public function withParentProjectPHIDs($parent_phids) {
$this->parentPHIDs = $parent_phids;
return $this;
}
public function withAncestorProjectPHIDs($ancestor_phids) {
$this->ancestorPHIDs = $ancestor_phids;
return $this;
}
public function withIsMilestone($is_milestone) {
$this->isMilestone = $is_milestone;
return $this;
}
public function withHasSubprojects($has_subprojects) {
$this->hasSubprojects = $has_subprojects;
return $this;
}
public function withDepthBetween($min, $max) {
$this->minDepth = $min;
$this->maxDepth = $max;
return $this;
}
public function withMilestoneNumberBetween($min, $max) {
$this->minMilestoneNumber = $min;
$this->maxMilestoneNumber = $max;
return $this;
}
public function withSubtypes(array $subtypes) {
$this->subtypes = $subtypes;
return $this;
}
public function needMembers($need_members) {
$this->needMembers = $need_members;
return $this;
}
public function needAncestorMembers($need_ancestor_members) {
$this->needAncestorMembers = $need_ancestor_members;
return $this;
}
public function needWatchers($need_watchers) {
$this->needWatchers = $need_watchers;
return $this;
}
public function needImages($need_images) {
$this->needImages = $need_images;
return $this;
}
public function needSlugs($need_slugs) {
$this->needSlugs = $need_slugs;
return $this;
}
public function newResultObject() {
return new PhabricatorProject();
}
protected function getDefaultOrderVector() {
return array('name');
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Name'),
),
) + parent::getBuiltinOrders();
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'name',
'reverse' => true,
'type' => 'string',
'unique' => true,
),
'milestoneNumber' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'milestoneNumber',
'type' => 'int',
),
'status' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'status',
'type' => 'int',
),
);
}
- protected function getPagingValueMap($cursor, array $keys) {
- $project = $this->loadCursorObject($cursor);
+ protected function newPagingMapFromPartialObject($object) {
return array(
- 'id' => $project->getID(),
- 'name' => $project->getName(),
- 'status' => $project->getStatus(),
+ 'id' => (int)$object->getID(),
+ 'name' => $object->getName(),
+ 'status' => $object->getStatus(),
);
}
public function getSlugMap() {
if ($this->slugMap === null) {
throw new PhutilInvalidStateException('execute');
}
return $this->slugMap;
}
protected function willExecute() {
$this->slugMap = array();
$this->slugNormals = array();
$this->allSlugs = array();
if ($this->slugs) {
foreach ($this->slugs as $slug) {
if (PhabricatorSlug::isValidProjectSlug($slug)) {
$normal = PhabricatorSlug::normalizeProjectSlug($slug);
$this->slugNormals[$slug] = $normal;
$this->allSlugs[$normal] = $normal;
}
// NOTE: At least for now, we query for the normalized slugs but also
// for the slugs exactly as entered. This allows older projects with
// slugs that are no longer valid to continue to work.
$this->allSlugs[$slug] = $slug;
}
}
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $projects) {
$ancestor_paths = array();
foreach ($projects as $project) {
foreach ($project->getAncestorProjectPaths() as $path) {
$ancestor_paths[$path] = $path;
}
}
if ($ancestor_paths) {
$ancestors = id(new PhabricatorProject())->loadAllWhere(
'projectPath IN (%Ls)',
$ancestor_paths);
} else {
$ancestors = array();
}
$projects = $this->linkProjectGraph($projects, $ancestors);
$viewer_phid = $this->getViewer()->getPHID();
$material_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST;
$watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST;
$types = array();
$types[] = $material_type;
if ($this->needWatchers) {
$types[] = $watcher_type;
}
$all_graph = $this->getAllReachableAncestors($projects);
// NOTE: Although we may not need much information about ancestors, we
// always need to test if the viewer is a member, because we will return
// ancestor projects to the policy filter via ExtendedPolicy calls. If
// we skip populating membership data on a parent, the policy framework
// will think the user is not a member of the parent project.
$all_sources = array();
foreach ($all_graph as $project) {
// For milestones, we need parent members.
if ($project->isMilestone()) {
$parent_phid = $project->getParentProjectPHID();
$all_sources[$parent_phid] = $parent_phid;
}
$phid = $project->getPHID();
$all_sources[$phid] = $phid;
}
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($all_sources)
->withEdgeTypes($types);
$need_all_edges =
$this->needMembers ||
$this->needWatchers ||
$this->needAncestorMembers;
// If we only need to know if the viewer is a member, we can restrict
// the query to just their PHID.
$any_edges = true;
if (!$need_all_edges) {
if ($viewer_phid) {
$edge_query->withDestinationPHIDs(array($viewer_phid));
} else {
// If we don't need members or watchers and don't have a viewer PHID
// (viewer is logged-out or omnipotent), they'll never be a member
// so we don't need to issue this query at all.
$any_edges = false;
}
}
if ($any_edges) {
$edge_query->execute();
}
$membership_projects = array();
foreach ($all_graph as $project) {
$project_phid = $project->getPHID();
if ($project->isMilestone()) {
$source_phids = array($project->getParentProjectPHID());
} else {
$source_phids = array($project_phid);
}
if ($any_edges) {
$member_phids = $edge_query->getDestinationPHIDs(
$source_phids,
array($material_type));
} else {
$member_phids = array();
}
if (in_array($viewer_phid, $member_phids)) {
$membership_projects[$project_phid] = $project;
}
if ($this->needMembers || $this->needAncestorMembers) {
$project->attachMemberPHIDs($member_phids);
}
if ($this->needWatchers) {
$watcher_phids = $edge_query->getDestinationPHIDs(
array($project_phid),
array($watcher_type));
$project->attachWatcherPHIDs($watcher_phids);
$project->setIsUserWatcher(
$viewer_phid,
in_array($viewer_phid, $watcher_phids));
}
}
// If we loaded ancestor members, we've already populated membership
// lists above, so we can skip this step.
if (!$this->needAncestorMembers) {
$member_graph = $this->getAllReachableAncestors($membership_projects);
foreach ($all_graph as $phid => $project) {
$is_member = isset($member_graph[$phid]);
$project->setIsUserMember($viewer_phid, $is_member);
}
}
return $projects;
}
protected function didFilterPage(array $projects) {
$viewer = $this->getViewer();
if ($this->needImages) {
$need_images = $projects;
// First, try to load custom profile images for any projects with custom
// images.
$file_phids = array();
foreach ($need_images as $key => $project) {
$image_phid = $project->getProfileImagePHID();
if ($image_phid) {
$file_phids[$key] = $image_phid;
}
}
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setParentQuery($this)
->setViewer($viewer)
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
foreach ($file_phids as $key => $image_phid) {
$file = idx($files, $image_phid);
if (!$file) {
continue;
}
$need_images[$key]->attachProfileImageFile($file);
unset($need_images[$key]);
}
}
// For projects with default images, or projects where the custom image
// failed to load, load a builtin image.
if ($need_images) {
$builtin_map = array();
$builtins = array();
foreach ($need_images as $key => $project) {
$icon = $project->getIcon();
$builtin_name = PhabricatorProjectIconSet::getIconImage($icon);
$builtin_name = 'projects/'.$builtin_name;
$builtin = id(new PhabricatorFilesOnDiskBuiltinFile())
->setName($builtin_name);
$builtin_key = $builtin->getBuiltinFileKey();
$builtins[] = $builtin;
$builtin_map[$key] = $builtin_key;
}
$builtin_files = PhabricatorFile::loadBuiltins(
$viewer,
$builtins);
foreach ($need_images as $key => $project) {
$builtin_key = $builtin_map[$key];
$builtin_file = $builtin_files[$builtin_key];
$project->attachProfileImageFile($builtin_file);
}
}
}
$this->loadSlugs($projects);
return $projects;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->status != self::STATUS_ANY) {
switch ($this->status) {
case self::STATUS_OPEN:
case self::STATUS_ACTIVE:
$filter = array(
PhabricatorProjectStatus::STATUS_ACTIVE,
);
break;
case self::STATUS_CLOSED:
case self::STATUS_ARCHIVED:
$filter = array(
PhabricatorProjectStatus::STATUS_ARCHIVED,
);
break;
default:
throw new Exception(
pht(
"Unknown project status '%s'!",
$this->status));
}
$where[] = qsprintf(
$conn,
'status IN (%Ld)',
$filter);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'status IN (%Ls)',
$this->statuses);
}
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->memberPHIDs !== null) {
$where[] = qsprintf(
$conn,
'e.dst IN (%Ls)',
$this->memberPHIDs);
}
if ($this->watcherPHIDs !== null) {
$where[] = qsprintf(
$conn,
'w.dst IN (%Ls)',
$this->watcherPHIDs);
}
if ($this->slugs !== null) {
$where[] = qsprintf(
$conn,
'slug.slug IN (%Ls)',
$this->allSlugs);
}
if ($this->names !== null) {
$where[] = qsprintf(
$conn,
'name IN (%Ls)',
$this->names);
}
if ($this->namePrefixes) {
$parts = array();
foreach ($this->namePrefixes as $name_prefix) {
$parts[] = qsprintf(
$conn,
'name LIKE %>',
$name_prefix);
}
$where[] = qsprintf($conn, '%LO', $parts);
}
if ($this->icons !== null) {
$where[] = qsprintf(
$conn,
'icon IN (%Ls)',
$this->icons);
}
if ($this->colors !== null) {
$where[] = qsprintf(
$conn,
'color IN (%Ls)',
$this->colors);
}
if ($this->parentPHIDs !== null) {
$where[] = qsprintf(
$conn,
'parentProjectPHID IN (%Ls)',
$this->parentPHIDs);
}
if ($this->ancestorPHIDs !== null) {
$ancestor_paths = queryfx_all(
$conn,
'SELECT projectPath, projectDepth FROM %T WHERE phid IN (%Ls)',
id(new PhabricatorProject())->getTableName(),
$this->ancestorPHIDs);
if (!$ancestor_paths) {
throw new PhabricatorEmptyQueryException();
}
$sql = array();
foreach ($ancestor_paths as $ancestor_path) {
$sql[] = qsprintf(
$conn,
'(projectPath LIKE %> AND projectDepth > %d)',
$ancestor_path['projectPath'],
$ancestor_path['projectDepth']);
}
$where[] = qsprintf($conn, '%LO', $sql);
$where[] = qsprintf(
$conn,
'parentProjectPHID IS NOT NULL');
}
if ($this->isMilestone !== null) {
if ($this->isMilestone) {
$where[] = qsprintf(
$conn,
'milestoneNumber IS NOT NULL');
} else {
$where[] = qsprintf(
$conn,
'milestoneNumber IS NULL');
}
}
if ($this->hasSubprojects !== null) {
$where[] = qsprintf(
$conn,
'hasSubprojects = %d',
(int)$this->hasSubprojects);
}
if ($this->minDepth !== null) {
$where[] = qsprintf(
$conn,
'projectDepth >= %d',
$this->minDepth);
}
if ($this->maxDepth !== null) {
$where[] = qsprintf(
$conn,
'projectDepth <= %d',
$this->maxDepth);
}
if ($this->minMilestoneNumber !== null) {
$where[] = qsprintf(
$conn,
'milestoneNumber >= %d',
$this->minMilestoneNumber);
}
if ($this->maxMilestoneNumber !== null) {
$where[] = qsprintf(
$conn,
'milestoneNumber <= %d',
$this->maxMilestoneNumber);
}
if ($this->subtypes !== null) {
$where[] = qsprintf(
$conn,
'subtype IN (%Ls)',
$this->subtypes);
}
return $where;
}
protected function shouldGroupQueryResultRows() {
if ($this->memberPHIDs || $this->watcherPHIDs || $this->nameTokens) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->memberPHIDs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T e ON e.src = p.phid AND e.type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorProjectMaterializedMemberEdgeType::EDGECONST);
}
if ($this->watcherPHIDs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T w ON w.src = p.phid AND w.type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorObjectHasWatcherEdgeType::EDGECONST);
}
if ($this->slugs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T slug on slug.projectPHID = p.phid',
id(new PhabricatorProjectSlug())->getTableName());
}
if ($this->nameTokens !== null) {
$name_tokens = $this->getNameTokensForQuery($this->nameTokens);
foreach ($name_tokens as $key => $token) {
$token_table = 'token_'.$key;
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.projectID = p.id AND %T.token LIKE %>',
PhabricatorProject::TABLE_DATASOURCE_TOKEN,
$token_table,
$token_table,
$token_table,
$token);
}
}
return $joins;
}
public function getQueryApplicationClass() {
return 'PhabricatorProjectApplication';
}
protected function getPrimaryTableAlias() {
return 'p';
}
private function linkProjectGraph(array $projects, array $ancestors) {
$ancestor_map = mpull($ancestors, null, 'getPHID');
$projects_map = mpull($projects, null, 'getPHID');
$all_map = $projects_map + $ancestor_map;
$done = array();
foreach ($projects as $key => $project) {
$seen = array($project->getPHID() => true);
if (!$this->linkProject($project, $all_map, $done, $seen)) {
$this->didRejectResult($project);
unset($projects[$key]);
continue;
}
foreach ($project->getAncestorProjects() as $ancestor) {
$seen[$ancestor->getPHID()] = true;
}
}
return $projects;
}
private function linkProject($project, array $all, array $done, array $seen) {
$parent_phid = $project->getParentProjectPHID();
// This project has no parent, so just attach `null` and return.
if (!$parent_phid) {
$project->attachParentProject(null);
return true;
}
// This project has a parent, but it failed to load.
if (empty($all[$parent_phid])) {
return false;
}
// Test for graph cycles. If we encounter one, we're going to hide the
// entire cycle since we can't meaningfully resolve it.
if (isset($seen[$parent_phid])) {
return false;
}
$seen[$parent_phid] = true;
$parent = $all[$parent_phid];
$project->attachParentProject($parent);
if (!empty($done[$parent_phid])) {
return true;
}
return $this->linkProject($parent, $all, $done, $seen);
}
private function getAllReachableAncestors(array $projects) {
$ancestors = array();
$seen = mpull($projects, null, 'getPHID');
$stack = $projects;
while ($stack) {
$project = array_pop($stack);
$phid = $project->getPHID();
$ancestors[$phid] = $project;
$parent_phid = $project->getParentProjectPHID();
if (!$parent_phid) {
continue;
}
if (isset($seen[$parent_phid])) {
continue;
}
$seen[$parent_phid] = true;
$stack[] = $project->getParentProject();
}
return $ancestors;
}
private function loadSlugs(array $projects) {
// Build a map from primary slugs to projects.
$primary_map = array();
foreach ($projects as $project) {
$primary_slug = $project->getPrimarySlug();
if ($primary_slug === null) {
continue;
}
$primary_map[$primary_slug] = $project;
}
// Link up all of the queried slugs which correspond to primary
// slugs. If we can link up everything from this (no slugs were queried,
// or only primary slugs were queried) we don't need to load anything
// else.
$unknown = $this->slugNormals;
foreach ($unknown as $input => $normal) {
if (isset($primary_map[$input])) {
$match = $input;
} else if (isset($primary_map[$normal])) {
$match = $normal;
} else {
continue;
}
$this->slugMap[$input] = array(
'slug' => $match,
'projectPHID' => $primary_map[$match]->getPHID(),
);
unset($unknown[$input]);
}
// If we need slugs, we have to load everything.
// If we still have some queried slugs which we haven't mapped, we only
// need to look for them.
// If we've mapped everything, we don't have to do any work.
$project_phids = mpull($projects, 'getPHID');
if ($this->needSlugs) {
$slugs = id(new PhabricatorProjectSlug())->loadAllWhere(
'projectPHID IN (%Ls)',
$project_phids);
} else if ($unknown) {
$slugs = id(new PhabricatorProjectSlug())->loadAllWhere(
'projectPHID IN (%Ls) AND slug IN (%Ls)',
$project_phids,
$unknown);
} else {
$slugs = array();
}
// Link up any slugs we were not able to link up earlier.
$extra_map = mpull($slugs, 'getProjectPHID', 'getSlug');
foreach ($unknown as $input => $normal) {
if (isset($extra_map[$input])) {
$match = $input;
} else if (isset($extra_map[$normal])) {
$match = $normal;
} else {
continue;
}
$this->slugMap[$input] = array(
'slug' => $match,
'projectPHID' => $extra_map[$match],
);
unset($unknown[$input]);
}
if ($this->needSlugs) {
$slug_groups = mgroup($slugs, 'getProjectPHID');
foreach ($projects as $project) {
$project_slugs = idx($slug_groups, $project->getPHID(), array());
$project->attachSlugs($project_slugs);
}
}
}
private function getNameTokensForQuery(array $tokens) {
// When querying for projects by name, only actually search for the five
// longest tokens. MySQL can get grumpy with a large number of JOINs
// with LIKEs and queries for more than 5 tokens are essentially never
// legitimate searches for projects, but users copy/pasting nonsense.
// See also PHI47.
$length_map = array();
foreach ($tokens as $token) {
$length_map[$token] = strlen($token);
}
arsort($length_map);
$length_map = array_slice($length_map, 0, 5, true);
return array_keys($length_map);
}
}
diff --git a/src/applications/project/query/PhabricatorProjectTriggerQuery.php b/src/applications/project/query/PhabricatorProjectTriggerQuery.php
new file mode 100644
index 000000000..452e3e53f
--- /dev/null
+++ b/src/applications/project/query/PhabricatorProjectTriggerQuery.php
@@ -0,0 +1,135 @@
+<?php
+
+final class PhabricatorProjectTriggerQuery
+ extends PhabricatorCursorPagedPolicyAwareQuery {
+
+ private $ids;
+ private $phids;
+ private $activeColumnMin;
+ private $activeColumnMax;
+
+ private $needUsage;
+
+ public function withIDs(array $ids) {
+ $this->ids = $ids;
+ return $this;
+ }
+
+ public function withPHIDs(array $phids) {
+ $this->phids = $phids;
+ return $this;
+ }
+
+ public function needUsage($need_usage) {
+ $this->needUsage = $need_usage;
+ return $this;
+ }
+
+ public function withActiveColumnCountBetween($min, $max) {
+ $this->activeColumnMin = $min;
+ $this->activeColumnMax = $max;
+ return $this;
+ }
+
+ public function newResultObject() {
+ return new PhabricatorProjectTrigger();
+ }
+
+ protected function loadPage() {
+ return $this->loadStandardPage($this->newResultObject());
+ }
+
+ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+ $where = parent::buildWhereClauseParts($conn);
+
+ if ($this->ids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'trigger.id IN (%Ld)',
+ $this->ids);
+ }
+
+ if ($this->phids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'trigger.phid IN (%Ls)',
+ $this->phids);
+ }
+
+ if ($this->activeColumnMin !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'trigger_usage.activeColumnCount >= %d',
+ $this->activeColumnMin);
+ }
+
+ if ($this->activeColumnMax !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'trigger_usage.activeColumnCount <= %d',
+ $this->activeColumnMax);
+ }
+
+ return $where;
+ }
+
+ protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
+ $joins = parent::buildJoinClauseParts($conn);
+
+ if ($this->shouldJoinUsageTable()) {
+ $joins[] = qsprintf(
+ $conn,
+ 'JOIN %R trigger_usage ON trigger.phid = trigger_usage.triggerPHID',
+ new PhabricatorProjectTriggerUsage());
+ }
+
+ return $joins;
+ }
+
+ private function shouldJoinUsageTable() {
+ if ($this->activeColumnMin !== null) {
+ return true;
+ }
+
+ if ($this->activeColumnMax !== null) {
+ return true;
+ }
+
+ return false;
+ }
+
+ protected function didFilterPage(array $triggers) {
+ if ($this->needUsage) {
+ $usage_map = id(new PhabricatorProjectTriggerUsage())->loadAllWhere(
+ 'triggerPHID IN (%Ls)',
+ mpull($triggers, 'getPHID'));
+ $usage_map = mpull($usage_map, null, 'getTriggerPHID');
+
+ foreach ($triggers as $trigger) {
+ $trigger_phid = $trigger->getPHID();
+
+ $usage = idx($usage_map, $trigger_phid);
+ if (!$usage) {
+ $usage = id(new PhabricatorProjectTriggerUsage())
+ ->setTriggerPHID($trigger_phid)
+ ->setExamplePHID(null)
+ ->setColumnCount(0)
+ ->setActiveColumnCount(0);
+ }
+
+ $trigger->attachUsage($usage);
+ }
+ }
+
+ return $triggers;
+ }
+
+ public function getQueryApplicationClass() {
+ return 'PhabricatorProjectApplication';
+ }
+
+ protected function getPrimaryTableAlias() {
+ return 'trigger';
+ }
+
+}
diff --git a/src/applications/project/query/PhabricatorProjectTriggerSearchEngine.php b/src/applications/project/query/PhabricatorProjectTriggerSearchEngine.php
new file mode 100644
index 000000000..a178ed3e6
--- /dev/null
+++ b/src/applications/project/query/PhabricatorProjectTriggerSearchEngine.php
@@ -0,0 +1,155 @@
+<?php
+
+final class PhabricatorProjectTriggerSearchEngine
+ extends PhabricatorApplicationSearchEngine {
+
+ public function getResultTypeDescription() {
+ return pht('Triggers');
+ }
+
+ public function getApplicationClassName() {
+ return 'PhabricatorProjectApplication';
+ }
+
+ public function newQuery() {
+ return id(new PhabricatorProjectTriggerQuery())
+ ->needUsage(true);
+ }
+
+ protected function buildCustomSearchFields() {
+ return array(
+ id(new PhabricatorSearchThreeStateField())
+ ->setLabel(pht('Active'))
+ ->setKey('isActive')
+ ->setOptions(
+ pht('(Show All)'),
+ pht('Show Only Active Triggers'),
+ pht('Show Only Inactive Triggers')),
+ );
+ }
+
+ protected function buildQueryFromParameters(array $map) {
+ $query = $this->newQuery();
+
+ if ($map['isActive'] !== null) {
+ if ($map['isActive']) {
+ $query->withActiveColumnCountBetween(1, null);
+ } else {
+ $query->withActiveColumnCountBetween(null, 0);
+ }
+ }
+
+ return $query;
+ }
+
+ protected function getURI($path) {
+ return '/project/trigger/'.$path;
+ }
+
+ protected function getBuiltinQueryNames() {
+ $names = array();
+
+ $names['active'] = pht('Active Triggers');
+ $names['all'] = pht('All Triggers');
+
+ return $names;
+ }
+
+ public function buildSavedQueryFromBuiltin($query_key) {
+ $query = $this->newSavedQuery();
+ $query->setQueryKey($query_key);
+
+ switch ($query_key) {
+ case 'active':
+ return $query->setParameter('isActive', true);
+ case 'all':
+ return $query;
+ }
+
+ return parent::buildSavedQueryFromBuiltin($query_key);
+ }
+
+ protected function renderResultList(
+ array $triggers,
+ PhabricatorSavedQuery $query,
+ array $handles) {
+ assert_instances_of($triggers, 'PhabricatorProjectTrigger');
+ $viewer = $this->requireViewer();
+
+ $example_phids = array();
+ foreach ($triggers as $trigger) {
+ $example_phid = $trigger->getUsage()->getExamplePHID();
+ if ($example_phid) {
+ $example_phids[] = $example_phid;
+ }
+ }
+
+ $handles = $viewer->loadHandles($example_phids);
+
+ $list = id(new PHUIObjectItemListView())
+ ->setViewer($viewer);
+ foreach ($triggers as $trigger) {
+ $usage = $trigger->getUsage();
+
+ $column_handle = null;
+ $have_column = false;
+ $example_phid = $usage->getExamplePHID();
+ if ($example_phid) {
+ $column_handle = $handles[$example_phid];
+ if ($column_handle->isComplete()) {
+ if (!$column_handle->getPolicyFiltered()) {
+ $have_column = true;
+ }
+ }
+ }
+
+ $column_count = $usage->getColumnCount();
+ $active_count = $usage->getActiveColumnCount();
+
+ if ($have_column) {
+ if ($active_count > 1) {
+ $usage_description = pht(
+ 'Used on %s and %s other active column(s).',
+ $column_handle->renderLink(),
+ new PhutilNumber($active_count - 1));
+ } else if ($column_count > 1) {
+ $usage_description = pht(
+ 'Used on %s and %s other column(s).',
+ $column_handle->renderLink(),
+ new PhutilNumber($column_count - 1));
+ } else {
+ $usage_description = pht(
+ 'Used on %s.',
+ $column_handle->renderLink());
+ }
+ } else {
+ if ($active_count) {
+ $usage_description = pht(
+ 'Used on %s active column(s).',
+ new PhutilNumber($active_count));
+ } else if ($column_count) {
+ $usage_description = pht(
+ 'Used on %s column(s).',
+ new PhutilNumber($column_count));
+ } else {
+ $usage_description = pht(
+ 'Unused trigger.');
+ }
+ }
+
+ $item = id(new PHUIObjectItemView())
+ ->setObjectName($trigger->getObjectName())
+ ->setHeader($trigger->getDisplayName())
+ ->setHref($trigger->getURI())
+ ->addAttribute($usage_description)
+ ->setDisabled(!$active_count);
+
+ $list->addItem($item);
+ }
+
+ return id(new PhabricatorApplicationSearchResultView())
+ ->setObjectList($list)
+ ->setNoDataString(pht('No triggers found.'));
+ }
+
+}
diff --git a/src/applications/project/query/PhabricatorProjectTriggerTransactionQuery.php b/src/applications/project/query/PhabricatorProjectTriggerTransactionQuery.php
new file mode 100644
index 000000000..9ec4d4a53
--- /dev/null
+++ b/src/applications/project/query/PhabricatorProjectTriggerTransactionQuery.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorProjectTriggerTransactionQuery
+ extends PhabricatorApplicationTransactionQuery {
+
+ public function getTemplateApplicationTransaction() {
+ return new PhabricatorProjectTriggerTransaction();
+ }
+
+}
diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php
index 5182a941b..67ab05f5f 100644
--- a/src/applications/project/storage/PhabricatorProject.php
+++ b/src/applications/project/storage/PhabricatorProject.php
@@ -1,907 +1,911 @@
<?php
final class PhabricatorProject extends PhabricatorProjectDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorConduitResultInterface,
PhabricatorColumnProxyInterface,
PhabricatorSpacesInterface,
PhabricatorEditEngineSubtypeInterface {
protected $name;
protected $status = PhabricatorProjectStatus::STATUS_ACTIVE;
protected $authorPHID;
protected $primarySlug;
protected $profileImagePHID;
protected $icon;
protected $color;
protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
protected $joinPolicy;
protected $isMembershipLocked;
protected $parentProjectPHID;
protected $hasWorkboard;
protected $hasMilestones;
protected $hasSubprojects;
protected $milestoneNumber;
protected $projectPath;
protected $projectDepth;
protected $projectPathKey;
protected $properties = array();
protected $spacePHID;
protected $subtype;
private $memberPHIDs = self::ATTACHABLE;
private $watcherPHIDs = self::ATTACHABLE;
private $sparseWatchers = self::ATTACHABLE;
private $sparseMembers = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $profileImageFile = self::ATTACHABLE;
private $slugs = self::ATTACHABLE;
private $parentProject = self::ATTACHABLE;
const TABLE_DATASOURCE_TOKEN = 'project_datasourcetoken';
const ITEM_PICTURE = 'project.picture';
const ITEM_PROFILE = 'project.profile';
const ITEM_POINTS = 'project.points';
const ITEM_WORKBOARD = 'project.workboard';
const ITEM_MEMBERS = 'project.members';
const ITEM_MANAGE = 'project.manage';
const ITEM_MILESTONES = 'project.milestones';
const ITEM_SUBPROJECTS = 'project.subprojects';
public static function initializeNewProject(
PhabricatorUser $actor,
PhabricatorProject $parent = null) {
$app = id(new PhabricatorApplicationQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withClasses(array('PhabricatorProjectApplication'))
->executeOne();
$view_policy = $app->getPolicy(
ProjectDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(
ProjectDefaultEditCapability::CAPABILITY);
$join_policy = $app->getPolicy(
ProjectDefaultJoinCapability::CAPABILITY);
// If this is the child of some other project, default the Space to the
// Space of the parent.
if ($parent) {
$space_phid = $parent->getSpacePHID();
} else {
$space_phid = $actor->getDefaultSpacePHID();
}
$default_icon = PhabricatorProjectIconSet::getDefaultIconKey();
$default_color = PhabricatorProjectIconSet::getDefaultColorKey();
return id(new PhabricatorProject())
->setAuthorPHID($actor->getPHID())
->setIcon($default_icon)
->setColor($default_color)
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setJoinPolicy($join_policy)
->setSpacePHID($space_phid)
->setIsMembershipLocked(0)
->attachMemberPHIDs(array())
->attachSlugs(array())
->setHasWorkboard(0)
->setHasMilestones(0)
->setHasSubprojects(0)
->setSubtype(PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT)
->attachParentProject(null);
}
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
PhabricatorPolicyCapability::CAN_JOIN,
);
}
public function getPolicy($capability) {
if ($this->isMilestone()) {
return $this->getParentProject()->getPolicy($capability);
}
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
case PhabricatorPolicyCapability::CAN_JOIN:
return $this->getJoinPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->isMilestone()) {
return $this->getParentProject()->hasAutomaticCapability(
$capability,
$viewer);
}
$can_edit = PhabricatorPolicyCapability::CAN_EDIT;
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if ($this->isUserMember($viewer->getPHID())) {
// Project members can always view a project.
return true;
}
break;
case PhabricatorPolicyCapability::CAN_EDIT:
$parent = $this->getParentProject();
if ($parent) {
$can_edit_parent = PhabricatorPolicyFilter::hasCapability(
$viewer,
$parent,
$can_edit);
if ($can_edit_parent) {
return true;
}
}
break;
case PhabricatorPolicyCapability::CAN_JOIN:
if (PhabricatorPolicyFilter::hasCapability($viewer, $this, $can_edit)) {
// Project editors can always join a project.
return true;
}
break;
}
return false;
}
public function describeAutomaticCapability($capability) {
// TODO: Clarify the additional rules that parent and subprojects imply.
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return pht('Members of a project can always view it.');
case PhabricatorPolicyCapability::CAN_JOIN:
return pht('Users who can edit a project can always join it.');
}
return null;
}
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
$extended = array();
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$parent = $this->getParentProject();
if ($parent) {
$extended[] = array(
$parent,
PhabricatorPolicyCapability::CAN_VIEW,
);
}
break;
}
return $extended;
}
public function isUserMember($user_phid) {
if ($this->memberPHIDs !== self::ATTACHABLE) {
return in_array($user_phid, $this->memberPHIDs);
}
return $this->assertAttachedKey($this->sparseMembers, $user_phid);
}
public function setIsUserMember($user_phid, $is_member) {
if ($this->sparseMembers === self::ATTACHABLE) {
$this->sparseMembers = array();
}
$this->sparseMembers[$user_phid] = $is_member;
return $this;
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort128',
'status' => 'text32',
'primarySlug' => 'text128?',
'isMembershipLocked' => 'bool',
'profileImagePHID' => 'phid?',
'icon' => 'text32',
'color' => 'text32',
'mailKey' => 'bytes20',
'joinPolicy' => 'policy',
'parentProjectPHID' => 'phid?',
'hasWorkboard' => 'bool',
'hasMilestones' => 'bool',
'hasSubprojects' => 'bool',
'milestoneNumber' => 'uint32?',
'projectPath' => 'hashpath64',
'projectDepth' => 'uint32',
'projectPathKey' => 'bytes4',
'subtype' => 'text64',
),
self::CONFIG_KEY_SCHEMA => array(
'key_icon' => array(
'columns' => array('icon'),
),
'key_color' => array(
'columns' => array('color'),
),
'key_milestone' => array(
'columns' => array('parentProjectPHID', 'milestoneNumber'),
'unique' => true,
),
'key_primaryslug' => array(
'columns' => array('primarySlug'),
'unique' => true,
),
'key_path' => array(
'columns' => array('projectPath', 'projectDepth'),
),
'key_pathkey' => array(
'columns' => array('projectPathKey'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorProjectProjectPHIDType::TYPECONST);
}
public function attachMemberPHIDs(array $phids) {
$this->memberPHIDs = $phids;
return $this;
}
public function getMemberPHIDs() {
return $this->assertAttached($this->memberPHIDs);
}
public function isArchived() {
return ($this->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED);
}
public function getProfileImageURI() {
return $this->getProfileImageFile()->getBestURI();
}
public function attachProfileImageFile(PhabricatorFile $file) {
$this->profileImageFile = $file;
return $this;
}
public function getProfileImageFile() {
return $this->assertAttached($this->profileImageFile);
}
public function isUserWatcher($user_phid) {
if ($this->watcherPHIDs !== self::ATTACHABLE) {
return in_array($user_phid, $this->watcherPHIDs);
}
return $this->assertAttachedKey($this->sparseWatchers, $user_phid);
}
public function isUserAncestorWatcher($user_phid) {
$is_watcher = $this->isUserWatcher($user_phid);
if (!$is_watcher) {
$parent = $this->getParentProject();
if ($parent) {
return $parent->isUserWatcher($user_phid);
}
}
return $is_watcher;
}
public function getWatchedAncestorPHID($user_phid) {
if ($this->isUserWatcher($user_phid)) {
return $this->getPHID();
}
$parent = $this->getParentProject();
if ($parent) {
return $parent->getWatchedAncestorPHID($user_phid);
}
return null;
}
public function setIsUserWatcher($user_phid, $is_watcher) {
if ($this->sparseWatchers === self::ATTACHABLE) {
$this->sparseWatchers = array();
}
$this->sparseWatchers[$user_phid] = $is_watcher;
return $this;
}
public function attachWatcherPHIDs(array $phids) {
$this->watcherPHIDs = $phids;
return $this;
}
public function getWatcherPHIDs() {
return $this->assertAttached($this->watcherPHIDs);
}
public function getAllAncestorWatcherPHIDs() {
$parent = $this->getParentProject();
if ($parent) {
$watchers = $parent->getAllAncestorWatcherPHIDs();
} else {
$watchers = array();
}
foreach ($this->getWatcherPHIDs() as $phid) {
$watchers[$phid] = $phid;
}
return $watchers;
}
public function attachSlugs(array $slugs) {
$this->slugs = $slugs;
return $this;
}
public function getSlugs() {
return $this->assertAttached($this->slugs);
}
public function getColor() {
if ($this->isArchived()) {
return PHUITagView::COLOR_DISABLED;
}
return $this->color;
}
public function getURI() {
$id = $this->getID();
return "/project/view/{$id}/";
}
public function getProfileURI() {
$id = $this->getID();
return "/project/profile/{$id}/";
}
+ public function getWorkboardURI() {
+ return urisprintf('/project/board/%d/', $this->getID());
+ }
+
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
if (!strlen($this->getPHID())) {
$this->setPHID($this->generatePHID());
}
if (!strlen($this->getProjectPathKey())) {
$hash = PhabricatorHash::digestForIndex($this->getPHID());
$hash = substr($hash, 0, 4);
$this->setProjectPathKey($hash);
}
$path = array();
$depth = 0;
if ($this->parentProjectPHID) {
$parent = $this->getParentProject();
$path[] = $parent->getProjectPath();
$depth = $parent->getProjectDepth() + 1;
}
$path[] = $this->getProjectPathKey();
$path = implode('', $path);
$limit = self::getProjectDepthLimit();
if ($depth >= $limit) {
throw new Exception(pht('Project depth is too great.'));
}
$this->setProjectPath($path);
$this->setProjectDepth($depth);
$this->openTransaction();
$result = parent::save();
$this->updateDatasourceTokens();
$this->saveTransaction();
return $result;
}
public static function getProjectDepthLimit() {
// This is limited by how many path hashes we can fit in the path
// column.
return 16;
}
public function updateDatasourceTokens() {
$table = self::TABLE_DATASOURCE_TOKEN;
$conn_w = $this->establishConnection('w');
$id = $this->getID();
$slugs = queryfx_all(
$conn_w,
'SELECT * FROM %T WHERE projectPHID = %s',
id(new PhabricatorProjectSlug())->getTableName(),
$this->getPHID());
$all_strings = ipull($slugs, 'slug');
$all_strings[] = $this->getDisplayName();
$all_strings = implode(' ', $all_strings);
$tokens = PhabricatorTypeaheadDatasource::tokenizeString($all_strings);
$sql = array();
foreach ($tokens as $token) {
$sql[] = qsprintf($conn_w, '(%d, %s)', $id, $token);
}
$this->openTransaction();
queryfx(
$conn_w,
'DELETE FROM %T WHERE projectID = %d',
$table,
$id);
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T (projectID, token) VALUES %LQ',
$table,
$chunk);
}
$this->saveTransaction();
}
public function isMilestone() {
return ($this->getMilestoneNumber() !== null);
}
public function getParentProject() {
return $this->assertAttached($this->parentProject);
}
public function attachParentProject(PhabricatorProject $project = null) {
$this->parentProject = $project;
return $this;
}
public function getAncestorProjectPaths() {
$parts = array();
$path = $this->getProjectPath();
$parent_length = (strlen($path) - 4);
for ($ii = $parent_length; $ii > 0; $ii -= 4) {
$parts[] = substr($path, 0, $ii);
}
return $parts;
}
public function getAncestorProjects() {
$ancestors = array();
$cursor = $this->getParentProject();
while ($cursor) {
$ancestors[] = $cursor;
$cursor = $cursor->getParentProject();
}
return $ancestors;
}
public function supportsEditMembers() {
if ($this->isMilestone()) {
return false;
}
if ($this->getHasSubprojects()) {
return false;
}
return true;
}
public function supportsMilestones() {
if ($this->isMilestone()) {
return false;
}
return true;
}
public function supportsSubprojects() {
if ($this->isMilestone()) {
return false;
}
return true;
}
public function loadNextMilestoneNumber() {
$current = queryfx_one(
$this->establishConnection('w'),
'SELECT MAX(milestoneNumber) n
FROM %T
WHERE parentProjectPHID = %s',
$this->getTableName(),
$this->getPHID());
if (!$current) {
$number = 1;
} else {
$number = (int)$current['n'] + 1;
}
return $number;
}
public function getDisplayName() {
$name = $this->getName();
// If this is a milestone, show it as "Parent > Sprint 99".
if ($this->isMilestone()) {
$name = pht(
'%s (%s)',
$this->getParentProject()->getName(),
$name);
}
return $name;
}
public function getDisplayIconKey() {
if ($this->isMilestone()) {
$key = PhabricatorProjectIconSet::getMilestoneIconKey();
} else {
$key = $this->getIcon();
}
return $key;
}
public function getDisplayIconIcon() {
$key = $this->getDisplayIconKey();
return PhabricatorProjectIconSet::getIconIcon($key);
}
public function getDisplayIconName() {
$key = $this->getDisplayIconKey();
return PhabricatorProjectIconSet::getIconName($key);
}
public function getDisplayColor() {
if ($this->isMilestone()) {
return $this->getParentProject()->getColor();
}
return $this->getColor();
}
public function getDisplayIconComposeIcon() {
$icon = $this->getDisplayIconIcon();
return $icon;
}
public function getDisplayIconComposeColor() {
$color = $this->getDisplayColor();
$map = array(
'grey' => 'charcoal',
'checkered' => 'backdrop',
);
return idx($map, $color, $color);
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getDefaultWorkboardSort() {
return $this->getProperty('workboard.sort.default');
}
public function setDefaultWorkboardSort($sort) {
return $this->setProperty('workboard.sort.default', $sort);
}
public function getDefaultWorkboardFilter() {
return $this->getProperty('workboard.filter.default');
}
public function setDefaultWorkboardFilter($filter) {
return $this->setProperty('workboard.filter.default', $filter);
}
public function getWorkboardBackgroundColor() {
return $this->getProperty('workboard.background');
}
public function setWorkboardBackgroundColor($color) {
return $this->setProperty('workboard.background', $color);
}
public function getDisplayWorkboardBackgroundColor() {
$color = $this->getWorkboardBackgroundColor();
if ($color === null) {
$parent = $this->getParentProject();
if ($parent) {
return $parent->getDisplayWorkboardBackgroundColor();
}
}
if ($color === 'none') {
$color = null;
}
return $color;
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('projects.fields');
}
public function getCustomFieldBaseClass() {
return 'PhabricatorProjectCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorProjectTransactionEditor();
}
public function getApplicationTransactionTemplate() {
return new PhabricatorProjectTransaction();
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
if ($this->isMilestone()) {
return $this->getParentProject()->getSpacePHID();
}
return $this->spacePHID;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$columns = id(new PhabricatorProjectColumn())
->loadAllWhere('projectPHID = %s', $this->getPHID());
foreach ($columns as $column) {
$engine->destroyObject($column);
}
$slugs = id(new PhabricatorProjectSlug())
->loadAllWhere('projectPHID = %s', $this->getPHID());
foreach ($slugs as $slug) {
$slug->delete();
}
$this->saveTransaction();
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhabricatorProjectFulltextEngine();
}
/* -( PhabricatorFerretInterface )--------------------------------------- */
public function newFerretEngine() {
return new PhabricatorProjectFerretEngine();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the project.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('slug')
->setType('string')
->setDescription(pht('Primary slug/hashtag.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('subtype')
->setType('string')
->setDescription(pht('Subtype of the project.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('milestone')
->setType('int?')
->setDescription(pht('For milestones, milestone sequence number.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('parent')
->setType('map<string, wild>?')
->setDescription(
pht(
'For subprojects and milestones, a brief description of the '.
'parent project.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('depth')
->setType('int')
->setDescription(
pht(
'For subprojects and milestones, depth of this project in the '.
'tree. Root projects have depth 0.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('icon')
->setType('map<string, wild>')
->setDescription(pht('Information about the project icon.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('color')
->setType('map<string, wild>')
->setDescription(pht('Information about the project color.')),
);
}
public function getFieldValuesForConduit() {
$color_key = $this->getColor();
$color_name = PhabricatorProjectIconSet::getColorName($color_key);
if ($this->isMilestone()) {
$milestone = (int)$this->getMilestoneNumber();
} else {
$milestone = null;
}
$parent = $this->getParentProject();
if ($parent) {
$parent_ref = $parent->getRefForConduit();
} else {
$parent_ref = null;
}
return array(
'name' => $this->getName(),
'slug' => $this->getPrimarySlug(),
'subtype' => $this->getSubtype(),
'milestone' => $milestone,
'depth' => (int)$this->getProjectDepth(),
'parent' => $parent_ref,
'icon' => array(
'key' => $this->getDisplayIconKey(),
'name' => $this->getDisplayIconName(),
'icon' => $this->getDisplayIconIcon(),
),
'color' => array(
'key' => $color_key,
'name' => $color_name,
),
);
}
public function getConduitSearchAttachments() {
return array(
id(new PhabricatorProjectsMembersSearchEngineAttachment())
->setAttachmentKey('members'),
id(new PhabricatorProjectsWatchersSearchEngineAttachment())
->setAttachmentKey('watchers'),
id(new PhabricatorProjectsAncestorsSearchEngineAttachment())
->setAttachmentKey('ancestors'),
);
}
/**
* Get an abbreviated representation of this project for use in providing
* "parent" and "ancestor" information.
*/
public function getRefForConduit() {
return array(
'id' => (int)$this->getID(),
'phid' => $this->getPHID(),
'name' => $this->getName(),
);
}
/* -( PhabricatorColumnProxyInterface )------------------------------------ */
public function getProxyColumnName() {
return $this->getName();
}
public function getProxyColumnIcon() {
return $this->getDisplayIconIcon();
}
public function getProxyColumnClass() {
if ($this->isMilestone()) {
return 'phui-workboard-column-milestone';
}
return null;
}
/* -( PhabricatorEditEngineSubtypeInterface )------------------------------ */
public function getEditEngineSubtype() {
return $this->getSubtype();
}
public function setEditEngineSubtype($value) {
return $this->setSubtype($value);
}
public function newEditEngineSubtypeMap() {
$config = PhabricatorEnv::getEnvConfig('projects.subtypes');
return PhabricatorEditEngineSubtype::newSubtypeMap($config);
}
public function newSubtypeObject() {
$subtype_key = $this->getEditEngineSubtype();
$subtype_map = $this->newEditEngineSubtypeMap();
return $subtype_map->getSubtype($subtype_key);
}
}
diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php
index 756c356ee..49d7f28a9 100644
--- a/src/applications/project/storage/PhabricatorProjectColumn.php
+++ b/src/applications/project/storage/PhabricatorProjectColumn.php
@@ -1,294 +1,362 @@
<?php
final class PhabricatorProjectColumn
extends PhabricatorProjectDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorExtendedPolicyInterface,
PhabricatorConduitResultInterface {
const STATUS_ACTIVE = 0;
const STATUS_HIDDEN = 1;
- const DEFAULT_ORDER = 'natural';
- const ORDER_NATURAL = 'natural';
- const ORDER_PRIORITY = 'priority';
-
protected $name;
protected $status;
protected $projectPHID;
protected $proxyPHID;
protected $sequence;
protected $properties = array();
+ protected $triggerPHID;
private $project = self::ATTACHABLE;
private $proxy = self::ATTACHABLE;
+ private $trigger = self::ATTACHABLE;
public static function initializeNewColumn(PhabricatorUser $user) {
return id(new PhabricatorProjectColumn())
->setName('')
->setStatus(self::STATUS_ACTIVE)
->attachProxy(null);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
'status' => 'uint32',
'sequence' => 'uint32',
'proxyPHID' => 'phid?',
+ 'triggerPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_status' => array(
'columns' => array('projectPHID', 'status', 'sequence'),
),
'key_sequence' => array(
'columns' => array('projectPHID', 'sequence'),
),
'key_proxy' => array(
'columns' => array('projectPHID', 'proxyPHID'),
'unique' => true,
),
+ 'key_trigger' => array(
+ 'columns' => array('triggerPHID'),
+ ),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorProjectColumnPHIDType::TYPECONST);
}
public function attachProject(PhabricatorProject $project) {
$this->project = $project;
return $this;
}
public function getProject() {
return $this->assertAttached($this->project);
}
public function attachProxy($proxy) {
$this->proxy = $proxy;
return $this;
}
public function getProxy() {
return $this->assertAttached($this->proxy);
}
public function isDefaultColumn() {
return (bool)$this->getProperty('isDefault');
}
public function isHidden() {
$proxy = $this->getProxy();
if ($proxy) {
return $proxy->isArchived();
}
return ($this->getStatus() == self::STATUS_HIDDEN);
}
public function getDisplayName() {
$proxy = $this->getProxy();
if ($proxy) {
return $proxy->getProxyColumnName();
}
$name = $this->getName();
if (strlen($name)) {
return $name;
}
if ($this->isDefaultColumn()) {
return pht('Backlog');
}
return pht('Unnamed Column');
}
public function getDisplayType() {
if ($this->isDefaultColumn()) {
return pht('(Default)');
}
if ($this->isHidden()) {
return pht('(Hidden)');
}
return null;
}
public function getDisplayClass() {
$proxy = $this->getProxy();
if ($proxy) {
return $proxy->getProxyColumnClass();
}
return null;
}
public function getHeaderIcon() {
$proxy = $this->getProxy();
if ($proxy) {
return $proxy->getProxyColumnIcon();
}
if ($this->isHidden()) {
return 'fa-eye-slash';
}
return null;
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getPointLimit() {
return $this->getProperty('pointLimit');
}
public function setPointLimit($limit) {
$this->setProperty('pointLimit', $limit);
return $this;
}
public function getOrderingKey() {
$proxy = $this->getProxy();
// Normal columns and subproject columns go first, in a user-controlled
// order.
// All the milestone columns go last, in their sequential order.
if (!$proxy || !$proxy->isMilestone()) {
$group = 'A';
$sequence = $this->getSequence();
} else {
$group = 'B';
$sequence = $proxy->getMilestoneNumber();
}
return sprintf('%s%012d', $group, $sequence);
}
+ public function attachTrigger(PhabricatorProjectTrigger $trigger = null) {
+ $this->trigger = $trigger;
+ return $this;
+ }
+
+ public function getTrigger() {
+ return $this->assertAttached($this->trigger);
+ }
+
+ public function canHaveTrigger() {
+ // Backlog columns and proxy (subproject / milestone) columns can't have
+ // triggers because cards routinely end up in these columns through tag
+ // edits rather than drag-and-drop and it would likely be confusing to
+ // have these triggers act only a small fraction of the time.
+
+ if ($this->isDefaultColumn()) {
+ return false;
+ }
+
+ if ($this->getProxy()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function getWorkboardURI() {
+ return $this->getProject()->getWorkboardURI();
+ }
+
+ public function getDropEffects() {
+ $effects = array();
+
+ $proxy = $this->getProxy();
+ if ($proxy && $proxy->isMilestone()) {
+ $effects[] = id(new PhabricatorProjectDropEffect())
+ ->setIcon($proxy->getProxyColumnIcon())
+ ->setColor('violet')
+ ->setContent(
+ pht(
+ 'Move to milestone %s.',
+ phutil_tag('strong', array(), $this->getDisplayName())));
+ } else {
+ $effects[] = id(new PhabricatorProjectDropEffect())
+ ->setIcon('fa-columns')
+ ->setColor('blue')
+ ->setContent(
+ pht(
+ 'Move to column %s.',
+ phutil_tag('strong', array(), $this->getDisplayName())));
+ }
+
+
+ if ($this->canHaveTrigger()) {
+ $trigger = $this->getTrigger();
+ if ($trigger) {
+ foreach ($trigger->getDropEffects() as $trigger_effect) {
+ $effects[] = $trigger_effect;
+ }
+ }
+ }
+
+ return $effects;
+ }
+
+
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The display name of the column.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('project')
->setType('map<string, wild>')
->setDescription(pht('The project the column belongs to.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('proxyPHID')
->setType('phid?')
->setDescription(
pht(
'For columns that proxy another object (like a subproject or '.
'milestone), the PHID of the object they proxy.')),
);
}
public function getFieldValuesForConduit() {
return array(
'name' => $this->getDisplayName(),
'proxyPHID' => $this->getProxyPHID(),
'project' => $this->getProject()->getRefForConduit(),
);
}
public function getConduitSearchAttachments() {
return array();
}
public function getRefForConduit() {
return array(
'id' => (int)$this->getID(),
'phid' => $this->getPHID(),
'name' => $this->getDisplayName(),
);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorProjectColumnTransactionEditor();
}
public function getApplicationTransactionTemplate() {
return new PhabricatorProjectColumnTransaction();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
// NOTE: Column policies are enforced as an extended policy which makes
// them the same as the project's policies.
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::POLICY_USER;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getProject()->hasAutomaticCapability(
$capability,
$viewer);
}
public function describeAutomaticCapability($capability) {
return pht('Users must be able to see a project to see its board.');
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
return array(
array($this->getProject(), $capability),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/project/storage/PhabricatorProjectColumnTransaction.php b/src/applications/project/storage/PhabricatorProjectColumnTransaction.php
index ed4bfed8a..35a7461ca 100644
--- a/src/applications/project/storage/PhabricatorProjectColumnTransaction.php
+++ b/src/applications/project/storage/PhabricatorProjectColumnTransaction.php
@@ -1,82 +1,18 @@
<?php
final class PhabricatorProjectColumnTransaction
- extends PhabricatorApplicationTransaction {
-
- const TYPE_NAME = 'project:col:name';
- const TYPE_STATUS = 'project:col:status';
- const TYPE_LIMIT = 'project:col:limit';
+ extends PhabricatorModularTransaction {
public function getApplicationName() {
return 'project';
}
public function getApplicationTransactionType() {
return PhabricatorProjectColumnPHIDType::TYPECONST;
}
- public function getTitle() {
- $old = $this->getOldValue();
- $new = $this->getNewValue();
- $author_handle = $this->renderHandleLink($this->getAuthorPHID());
-
- switch ($this->getTransactionType()) {
- case self::TYPE_NAME:
- if ($old === null) {
- return pht(
- '%s created this column.',
- $author_handle);
- } else {
- if (!strlen($old)) {
- return pht(
- '%s named this column "%s".',
- $author_handle,
- $new);
- } else if (strlen($new)) {
- return pht(
- '%s renamed this column from "%s" to "%s".',
- $author_handle,
- $old,
- $new);
- } else {
- return pht(
- '%s removed the custom name of this column.',
- $author_handle);
- }
- }
- case self::TYPE_LIMIT:
- if (!$old) {
- return pht(
- '%s set the point limit for this column to %s.',
- $author_handle,
- $new);
- } else if (!$new) {
- return pht(
- '%s removed the point limit for this column.',
- $author_handle);
- } else {
- return pht(
- '%s changed point limit for this column from %s to %s.',
- $author_handle,
- $old,
- $new);
- }
-
- case self::TYPE_STATUS:
- switch ($new) {
- case PhabricatorProjectColumn::STATUS_ACTIVE:
- return pht(
- '%s marked this column visible.',
- $author_handle);
- case PhabricatorProjectColumn::STATUS_HIDDEN:
- return pht(
- '%s marked this column hidden.',
- $author_handle);
- }
- break;
- }
-
- return parent::getTitle();
+ public function getBaseTransactionClass() {
+ return 'PhabricatorProjectColumnTransactionType';
}
}
diff --git a/src/applications/project/storage/PhabricatorProjectTransaction.php b/src/applications/project/storage/PhabricatorProjectTransaction.php
index 391b38364..e723fd1e6 100644
--- a/src/applications/project/storage/PhabricatorProjectTransaction.php
+++ b/src/applications/project/storage/PhabricatorProjectTransaction.php
@@ -1,177 +1,173 @@
<?php
final class PhabricatorProjectTransaction
extends PhabricatorModularTransaction {
// NOTE: This is deprecated, members are just a normal edge now.
const TYPE_MEMBERS = 'project:members';
const MAILTAG_METADATA = 'project-metadata';
const MAILTAG_MEMBERS = 'project-members';
const MAILTAG_WATCHERS = 'project-watchers';
const MAILTAG_OTHER = 'project-other';
// c4science customization
public function shouldHideForFeed() {
$c4s_hide = $this->getMetadataValue('c4s:hide');
if(isset($c4s_hide)) {
return $c4s_hide;
}
return parent::shouldHideForFeed();
}
public function getApplicationName() {
return 'project';
}
public function getApplicationTransactionType() {
return PhabricatorProjectProjectPHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return null;
- }
-
public function getBaseTransactionClass() {
return 'PhabricatorProjectTransactionType';
}
public function getRequiredHandlePHIDs() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$req_phids = array();
switch ($this->getTransactionType()) {
case self::TYPE_MEMBERS:
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
$req_phids = array_merge($add, $rem);
break;
}
return array_merge($req_phids, parent::getRequiredHandlePHIDs());
}
public function shouldHide() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $this->getMetadataValue('edge:type');
switch ($edge_type) {
case PhabricatorProjectSilencedEdgeType::EDGECONST:
return true;
default:
break;
}
}
return parent::shouldHide();
}
public function shouldHideForMail(array $xactions) {
switch ($this->getTransactionType()) {
case PhabricatorProjectWorkboardTransaction::TRANSACTIONTYPE:
case PhabricatorProjectSortTransaction::TRANSACTIONTYPE:
case PhabricatorProjectFilterTransaction::TRANSACTIONTYPE:
case PhabricatorProjectWorkboardBackgroundTransaction::TRANSACTIONTYPE:
return true;
}
return parent::shouldHideForMail($xactions);
}
public function getIcon() {
switch ($this->getTransactionType()) {
case self::TYPE_MEMBERS:
return 'fa-user';
}
return parent::getIcon();
}
public function getTitle() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$author_phid = $this->getAuthorPHID();
$author_handle = $this->renderHandleLink($author_phid);
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CREATE:
return pht(
'%s created this project.',
$this->renderHandleLink($author_phid));
case self::TYPE_MEMBERS:
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
if ($add && $rem) {
return pht(
'%s changed project member(s), added %d: %s; removed %d: %s.',
$author_handle,
count($add),
$this->renderHandleList($add),
count($rem),
$this->renderHandleList($rem));
} else if ($add) {
if (count($add) == 1 && (head($add) == $this->getAuthorPHID())) {
return pht(
'%s joined this project.',
$author_handle);
} else {
return pht(
'%s added %d project member(s): %s.',
$author_handle,
count($add),
$this->renderHandleList($add));
}
} else if ($rem) {
if (count($rem) == 1 && (head($rem) == $this->getAuthorPHID())) {
return pht(
'%s left this project.',
$author_handle);
} else {
return pht(
'%s removed %d project member(s): %s.',
$author_handle,
count($rem),
$this->renderHandleList($rem));
}
}
break;
}
return parent::getTitle();
}
public function getMailTags() {
$tags = array();
switch ($this->getTransactionType()) {
case PhabricatorProjectNameTransaction::TRANSACTIONTYPE:
case PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE:
case PhabricatorProjectImageTransaction::TRANSACTIONTYPE:
case PhabricatorProjectIconTransaction::TRANSACTIONTYPE:
case PhabricatorProjectColorTransaction::TRANSACTIONTYPE:
$tags[] = self::MAILTAG_METADATA;
break;
case PhabricatorTransactions::TYPE_EDGE:
$type = $this->getMetadata('edge:type');
$type = head($type);
$type_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
$type_watcher = PhabricatorObjectHasWatcherEdgeType::EDGECONST;
if ($type == $type_member) {
$tags[] = self::MAILTAG_MEMBERS;
} else if ($type == $type_watcher) {
$tags[] = self::MAILTAG_WATCHERS;
} else {
$tags[] = self::MAILTAG_OTHER;
}
break;
case PhabricatorProjectStatusTransaction::TRANSACTIONTYPE:
case PhabricatorProjectLockTransaction::TRANSACTIONTYPE:
default:
$tags[] = self::MAILTAG_OTHER;
break;
}
return $tags;
}
}
diff --git a/src/applications/project/storage/PhabricatorProjectTrigger.php b/src/applications/project/storage/PhabricatorProjectTrigger.php
new file mode 100644
index 000000000..625dc7ffd
--- /dev/null
+++ b/src/applications/project/storage/PhabricatorProjectTrigger.php
@@ -0,0 +1,336 @@
+<?php
+
+final class PhabricatorProjectTrigger
+ extends PhabricatorProjectDAO
+ implements
+ PhabricatorApplicationTransactionInterface,
+ PhabricatorPolicyInterface,
+ PhabricatorIndexableInterface,
+ PhabricatorDestructibleInterface {
+
+ protected $name;
+ protected $ruleset = array();
+ protected $editPolicy;
+
+ private $triggerRules;
+ private $usage = self::ATTACHABLE;
+
+ public static function initializeNewTrigger() {
+ $default_edit = PhabricatorPolicies::POLICY_USER;
+
+ return id(new self())
+ ->setName('')
+ ->setEditPolicy($default_edit);
+ }
+
+ protected function getConfiguration() {
+ return array(
+ self::CONFIG_AUX_PHID => true,
+ self::CONFIG_SERIALIZATION => array(
+ 'ruleset' => self::SERIALIZATION_JSON,
+ ),
+ self::CONFIG_COLUMN_SCHEMA => array(
+ 'name' => 'text255',
+ ),
+ self::CONFIG_KEY_SCHEMA => array(
+ ),
+ ) + parent::getConfiguration();
+ }
+
+ public function getPHIDType() {
+ return PhabricatorProjectTriggerPHIDType::TYPECONST;
+ }
+
+ public function getDisplayName() {
+ $name = $this->getName();
+ if (strlen($name)) {
+ return $name;
+ }
+
+ return $this->getDefaultName();
+ }
+
+ public function getDefaultName() {
+ return pht('Custom Trigger');
+ }
+
+ public function getURI() {
+ return urisprintf(
+ '/project/trigger/%d/',
+ $this->getID());
+ }
+
+ public function getObjectName() {
+ return pht('Trigger %d', $this->getID());
+ }
+
+ public function setRuleset(array $ruleset) {
+ // Clear any cached trigger rules, since we're changing the ruleset
+ // for the trigger.
+ $this->triggerRules = null;
+
+ parent::setRuleset($ruleset);
+ }
+
+ public function getTriggerRules() {
+ if ($this->triggerRules === null) {
+ $trigger_rules = self::newTriggerRulesFromRuleSpecifications(
+ $this->getRuleset(),
+ $allow_invalid = true);
+
+ $this->triggerRules = $trigger_rules;
+ }
+
+ return $this->triggerRules;
+ }
+
+ public static function newTriggerRulesFromRuleSpecifications(
+ array $list,
+ $allow_invalid) {
+
+ // NOTE: With "$allow_invalid" set, we're trying to preserve the database
+ // state in the rule structure, even if it includes rule types we don't
+ // ha ve implementations for, or rules with invalid rule values.
+
+ // If an administrator adds or removes extensions which add rules, or
+ // an upgrade affects rule validity, existing rules may become invalid.
+ // When they do, we still want the UI to reflect the ruleset state
+ // accurately and "Edit" + "Save" shouldn't destroy data unless the
+ // user explicitly modifies the ruleset.
+
+ // In this mode, when we run into rules which are structured correctly but
+ // which have types we don't know about, we replace them with "Unknown
+ // Rules". If we know about the type of a rule but the value doesn't
+ // validate, we replace it with "Invalid Rules". These two rule types don't
+ // take any actions when a card is dropped into the column, but they show
+ // the user what's wrong with the ruleset and can be saved without causing
+ // any collateral damage.
+
+ $rule_map = PhabricatorProjectTriggerRule::getAllTriggerRules();
+
+ // If the stored rule data isn't a list of rules (or we encounter other
+ // fundamental structural problems, below), there isn't much we can do
+ // to try to represent the state.
+ if (!is_array($list)) {
+ throw new PhabricatorProjectTriggerCorruptionException(
+ pht(
+ 'Trigger ruleset is corrupt: expected a list of rule '.
+ 'specifications, found "%s".',
+ phutil_describe_type($list)));
+ }
+
+ $trigger_rules = array();
+ foreach ($list as $key => $rule) {
+ if (!is_array($rule)) {
+ throw new PhabricatorProjectTriggerCorruptionException(
+ pht(
+ 'Trigger ruleset is corrupt: rule (with key "%s") should be a '.
+ 'rule specification, but is actually "%s".',
+ $key,
+ phutil_describe_type($rule)));
+ }
+
+ try {
+ PhutilTypeSpec::checkMap(
+ $rule,
+ array(
+ 'type' => 'string',
+ 'value' => 'wild',
+ ));
+ } catch (PhutilTypeCheckException $ex) {
+ throw new PhabricatorProjectTriggerCorruptionException(
+ pht(
+ 'Trigger ruleset is corrupt: rule (with key "%s") is not a '.
+ 'valid rule specification: %s',
+ $key,
+ $ex->getMessage()));
+ }
+
+ $record = id(new PhabricatorProjectTriggerRuleRecord())
+ ->setType(idx($rule, 'type'))
+ ->setValue(idx($rule, 'value'));
+
+ if (!isset($rule_map[$record->getType()])) {
+ if (!$allow_invalid) {
+ throw new PhabricatorProjectTriggerCorruptionException(
+ pht(
+ 'Trigger ruleset is corrupt: rule type "%s" is unknown.',
+ $record->getType()));
+ }
+
+ $rule = new PhabricatorProjectTriggerUnknownRule();
+ } else {
+ $rule = clone $rule_map[$record->getType()];
+ }
+
+ try {
+ $rule->setRecord($record);
+ } catch (Exception $ex) {
+ if (!$allow_invalid) {
+ throw new PhabricatorProjectTriggerCorruptionException(
+ pht(
+ 'Trigger ruleset is corrupt, rule (of type "%s") does not '.
+ 'validate: %s',
+ $record->getType(),
+ $ex->getMessage()));
+ }
+
+ $rule = id(new PhabricatorProjectTriggerInvalidRule())
+ ->setRecord($record)
+ ->setException($ex);
+ }
+
+ $trigger_rules[] = $rule;
+ }
+
+ return $trigger_rules;
+ }
+
+
+ public function getDropEffects() {
+ $effects = array();
+
+ $rules = $this->getTriggerRules();
+ foreach ($rules as $rule) {
+ foreach ($rule->getDropEffects() as $effect) {
+ $effects[] = $effect;
+ }
+ }
+
+ return $effects;
+ }
+
+ public function newDropTransactions(
+ PhabricatorUser $viewer,
+ PhabricatorProjectColumn $column,
+ $object) {
+
+ $trigger_xactions = array();
+ foreach ($this->getTriggerRules() as $rule) {
+ $rule
+ ->setViewer($viewer)
+ ->setTrigger($this)
+ ->setColumn($column)
+ ->setObject($object);
+
+ $xactions = $rule->getDropTransactions(
+ $object,
+ $rule->getRecord()->getValue());
+
+ if (!is_array($xactions)) {
+ throw new Exception(
+ pht(
+ 'Expected trigger rule (of class "%s") to return a list of '.
+ 'transactions from "newDropTransactions()", but got "%s".',
+ get_class($rule),
+ phutil_describe_type($xactions)));
+ }
+
+ $expect_type = get_class($object->getApplicationTransactionTemplate());
+ assert_instances_of($xactions, $expect_type);
+
+ foreach ($xactions as $xaction) {
+ $trigger_xactions[] = $xaction;
+ }
+ }
+
+ return $trigger_xactions;
+ }
+
+ public function getPreviewEffect() {
+ $header = pht('Trigger: %s', $this->getDisplayName());
+
+ return id(new PhabricatorProjectDropEffect())
+ ->setIcon('fa-cogs')
+ ->setColor('blue')
+ ->setIsHeader(true)
+ ->setContent($header);
+ }
+
+ public function getSoundEffects() {
+ $sounds = array();
+
+ foreach ($this->getTriggerRules() as $rule) {
+ foreach ($rule->getSoundEffects() as $effect) {
+ $sounds[] = $effect;
+ }
+ }
+
+ return $sounds;
+ }
+
+ public function getUsage() {
+ return $this->assertAttached($this->usage);
+ }
+
+ public function attachUsage(PhabricatorProjectTriggerUsage $usage) {
+ $this->usage = $usage;
+ return $this;
+ }
+
+
+/* -( PhabricatorApplicationTransactionInterface )------------------------- */
+
+
+ public function getApplicationTransactionEditor() {
+ return new PhabricatorProjectTriggerEditor();
+ }
+
+ public function getApplicationTransactionTemplate() {
+ return new PhabricatorProjectTriggerTransaction();
+ }
+
+
+/* -( PhabricatorPolicyInterface )----------------------------------------- */
+
+
+ public function getCapabilities() {
+ return array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ );
+ }
+
+ public function getPolicy($capability) {
+ switch ($capability) {
+ case PhabricatorPolicyCapability::CAN_VIEW:
+ return PhabricatorPolicies::getMostOpenPolicy();
+ case PhabricatorPolicyCapability::CAN_EDIT:
+ return $this->getEditPolicy();
+ }
+ }
+
+ public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
+ return false;
+ }
+
+
+/* -( PhabricatorDestructibleInterface )----------------------------------- */
+
+
+ public function destroyObjectPermanently(
+ PhabricatorDestructionEngine $engine) {
+
+ $this->openTransaction();
+ $conn = $this->establishConnection('w');
+
+ // Remove the reference to this trigger from any columns which use it.
+ queryfx(
+ $conn,
+ 'UPDATE %R SET triggerPHID = null WHERE triggerPHID = %s',
+ new PhabricatorProjectColumn(),
+ $this->getPHID());
+
+ // Remove the usage index row for this trigger, if one exists.
+ queryfx(
+ $conn,
+ 'DELETE FROM %R WHERE triggerPHID = %s',
+ new PhabricatorProjectTriggerUsage(),
+ $this->getPHID());
+
+ $this->delete();
+
+ $this->saveTransaction();
+ }
+
+}
diff --git a/src/applications/project/storage/PhabricatorProjectTriggerTransaction.php b/src/applications/project/storage/PhabricatorProjectTriggerTransaction.php
new file mode 100644
index 000000000..fb94bdc36
--- /dev/null
+++ b/src/applications/project/storage/PhabricatorProjectTriggerTransaction.php
@@ -0,0 +1,18 @@
+<?php
+
+final class PhabricatorProjectTriggerTransaction
+ extends PhabricatorModularTransaction {
+
+ public function getApplicationName() {
+ return 'project';
+ }
+
+ public function getApplicationTransactionType() {
+ return PhabricatorProjectTriggerPHIDType::TYPECONST;
+ }
+
+ public function getBaseTransactionClass() {
+ return 'PhabricatorProjectTriggerTransactionType';
+ }
+
+}
diff --git a/src/applications/project/storage/PhabricatorProjectTriggerUsage.php b/src/applications/project/storage/PhabricatorProjectTriggerUsage.php
new file mode 100644
index 000000000..982b8ecf5
--- /dev/null
+++ b/src/applications/project/storage/PhabricatorProjectTriggerUsage.php
@@ -0,0 +1,28 @@
+<?php
+
+final class PhabricatorProjectTriggerUsage
+ extends PhabricatorProjectDAO {
+
+ protected $triggerPHID;
+ protected $examplePHID;
+ protected $columnCount;
+ protected $activeColumnCount;
+
+ protected function getConfiguration() {
+ return array(
+ self::CONFIG_TIMESTAMPS => false,
+ self::CONFIG_COLUMN_SCHEMA => array(
+ 'examplePHID' => 'phid?',
+ 'columnCount' => 'uint32',
+ 'activeColumnCount' => 'uint32',
+ ),
+ self::CONFIG_KEY_SCHEMA => array(
+ 'key_trigger' => array(
+ 'columns' => array('triggerPHID'),
+ 'unique' => true,
+ ),
+ ),
+ ) + parent::getConfiguration();
+ }
+
+}
diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php
new file mode 100644
index 000000000..ba53b77e7
--- /dev/null
+++ b/src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php
@@ -0,0 +1,93 @@
+<?php
+
+final class PhabricatorProjectTriggerInvalidRule
+ extends PhabricatorProjectTriggerRule {
+
+ const TRIGGERTYPE = 'invalid';
+
+ private $exception;
+
+ public function setException(Exception $exception) {
+ $this->exception = $exception;
+ return $this;
+ }
+
+ public function getException() {
+ return $this->exception;
+ }
+
+ public function getSelectControlName() {
+ return pht('(Invalid Rule)');
+ }
+
+ protected function isSelectableRule() {
+ return false;
+ }
+
+ protected function assertValidRuleValue($value) {
+ return;
+ }
+
+ protected function newDropTransactions($object, $value) {
+ return array();
+ }
+
+ protected function newDropEffects($value) {
+ return array();
+ }
+
+ protected function isValidRule() {
+ return false;
+ }
+
+ protected function newInvalidView() {
+ return array(
+ id(new PHUIIconView())
+ ->setIcon('fa-exclamation-triangle red'),
+ ' ',
+ pht(
+ 'This is a trigger rule with a valid type ("%s") but an invalid '.
+ 'value.',
+ $this->getRecord()->getType()),
+ );
+ }
+
+ protected function getDefaultValue() {
+ return null;
+ }
+
+ protected function getPHUIXControlType() {
+ return null;
+ }
+
+ protected function getPHUIXControlSpecification() {
+ return null;
+ }
+
+ public function getRuleViewLabel() {
+ return pht('Invalid Rule');
+ }
+
+ public function getRuleViewDescription($value) {
+ $record = $this->getRecord();
+ $type = $record->getType();
+
+ $exception = $this->getException();
+ if ($exception) {
+ return pht(
+ 'This rule (of type "%s") is invalid: %s',
+ $type,
+ $exception->getMessage());
+ } else {
+ return pht(
+ 'This rule (of type "%s") is invalid.',
+ $type);
+ }
+ }
+
+ public function getRuleViewIcon($value) {
+ return id(new PHUIIconView())
+ ->setIcon('fa-exclamation-triangle', 'red');
+ }
+
+}
diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerManiphestPriorityRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerManiphestPriorityRule.php
new file mode 100644
index 000000000..98a03a139
--- /dev/null
+++ b/src/applications/project/trigger/PhabricatorProjectTriggerManiphestPriorityRule.php
@@ -0,0 +1,94 @@
+<?php
+
+final class PhabricatorProjectTriggerManiphestPriorityRule
+ extends PhabricatorProjectTriggerRule {
+
+ const TRIGGERTYPE = 'task.priority';
+
+ public function getSelectControlName() {
+ return pht('Change priority to');
+ }
+
+ protected function assertValidRuleValue($value) {
+ if (!is_string($value)) {
+ throw new Exception(
+ pht(
+ 'Priority rule value should be a string, but is not (value is "%s").',
+ phutil_describe_type($value)));
+ }
+
+ $map = ManiphestTaskPriority::getTaskPriorityMap();
+ if (!isset($map[$value])) {
+ throw new Exception(
+ pht(
+ 'Rule value ("%s") is not a valid task priority.',
+ $value));
+ }
+ }
+
+ protected function newDropTransactions($object, $value) {
+ $value = ManiphestTaskPriority::getKeywordForTaskPriority($value);
+ return array(
+ $this->newTransaction()
+ ->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE)
+ ->setNewValue($value),
+ );
+ }
+
+ protected function newDropEffects($value) {
+ $priority_name = ManiphestTaskPriority::getTaskPriorityName($value);
+ $priority_icon = ManiphestTaskPriority::getTaskPriorityIcon($value);
+ $priority_color = ManiphestTaskPriority::getTaskPriorityColor($value);
+
+ $content = pht(
+ 'Change priority to %s.',
+ phutil_tag('strong', array(), $priority_name));
+
+ return array(
+ $this->newEffect()
+ ->setIcon($priority_icon)
+ ->setColor($priority_color)
+ ->addCondition('priority', '!=', $value)
+ ->setContent($content),
+ );
+ }
+
+ protected function getDefaultValue() {
+ return head_key(ManiphestTaskPriority::getTaskPriorityMap());
+ }
+
+ protected function getPHUIXControlType() {
+ return 'select';
+ }
+
+ protected function getPHUIXControlSpecification() {
+ $map = ManiphestTaskPriority::getTaskPriorityMap();
+
+ return array(
+ 'options' => $map,
+ 'order' => array_keys($map),
+ );
+ }
+
+ public function getRuleViewLabel() {
+ return pht('Change Priority');
+ }
+
+ public function getRuleViewDescription($value) {
+ $priority_name = ManiphestTaskPriority::getTaskPriorityName($value);
+
+ return pht(
+ 'Change task priority to %s.',
+ phutil_tag('strong', array(), $priority_name));
+ }
+
+ public function getRuleViewIcon($value) {
+ $priority_icon = ManiphestTaskPriority::getTaskPriorityIcon($value);
+ $priority_color = ManiphestTaskPriority::getTaskPriorityColor($value);
+
+ return id(new PHUIIconView())
+ ->setIcon($priority_icon, $priority_color);
+ }
+
+
+}
diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php
new file mode 100644
index 000000000..b11d7567d
--- /dev/null
+++ b/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php
@@ -0,0 +1,93 @@
+<?php
+
+final class PhabricatorProjectTriggerManiphestStatusRule
+ extends PhabricatorProjectTriggerRule {
+
+ const TRIGGERTYPE = 'task.status';
+
+ public function getSelectControlName() {
+ return pht('Change status to');
+ }
+
+ protected function assertValidRuleValue($value) {
+ if (!is_string($value)) {
+ throw new Exception(
+ pht(
+ 'Status rule value should be a string, but is not (value is "%s").',
+ phutil_describe_type($value)));
+ }
+
+ $map = ManiphestTaskStatus::getTaskStatusMap();
+ if (!isset($map[$value])) {
+ throw new Exception(
+ pht(
+ 'Rule value ("%s") is not a valid task status.',
+ $value));
+ }
+ }
+
+ protected function newDropTransactions($object, $value) {
+ return array(
+ $this->newTransaction()
+ ->setTransactionType(ManiphestTaskStatusTransaction::TRANSACTIONTYPE)
+ ->setNewValue($value),
+ );
+ }
+
+ protected function newDropEffects($value) {
+ $status_name = ManiphestTaskStatus::getTaskStatusName($value);
+ $status_icon = ManiphestTaskStatus::getStatusIcon($value);
+ $status_color = ManiphestTaskStatus::getStatusColor($value);
+
+ $content = pht(
+ 'Change status to %s.',
+ phutil_tag('strong', array(), $status_name));
+
+ return array(
+ $this->newEffect()
+ ->setIcon($status_icon)
+ ->setColor($status_color)
+ ->addCondition('status', '!=', $value)
+ ->setContent($content),
+ );
+ }
+
+ protected function getDefaultValue() {
+ return head_key(ManiphestTaskStatus::getTaskStatusMap());
+ }
+
+ protected function getPHUIXControlType() {
+ return 'select';
+ }
+
+ protected function getPHUIXControlSpecification() {
+ $map = ManiphestTaskStatus::getTaskStatusMap();
+
+ return array(
+ 'options' => $map,
+ 'order' => array_keys($map),
+ );
+ }
+
+ public function getRuleViewLabel() {
+ return pht('Change Status');
+ }
+
+ public function getRuleViewDescription($value) {
+ $status_name = ManiphestTaskStatus::getTaskStatusName($value);
+
+ return pht(
+ 'Change task status to %s.',
+ phutil_tag('strong', array(), $status_name));
+ }
+
+ public function getRuleViewIcon($value) {
+ $status_icon = ManiphestTaskStatus::getStatusIcon($value);
+ $status_color = ManiphestTaskStatus::getStatusColor($value);
+
+ return id(new PHUIIconView())
+ ->setIcon($status_icon, $status_color);
+ }
+
+
+}
diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerPlaySoundRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerPlaySoundRule.php
new file mode 100644
index 000000000..ef19b504e
--- /dev/null
+++ b/src/applications/project/trigger/PhabricatorProjectTriggerPlaySoundRule.php
@@ -0,0 +1,122 @@
+<?php
+
+final class PhabricatorProjectTriggerPlaySoundRule
+ extends PhabricatorProjectTriggerRule {
+
+ const TRIGGERTYPE = 'sound';
+
+ public function getSelectControlName() {
+ return pht('Play sound');
+ }
+
+ protected function assertValidRuleValue($value) {
+ if (!is_string($value)) {
+ throw new Exception(
+ pht(
+ 'Status rule value should be a string, but is not (value is "%s").',
+ phutil_describe_type($value)));
+ }
+
+ $map = self::getSoundMap();
+
+ if (!isset($map[$value])) {
+ throw new Exception(
+ pht(
+ 'Rule value ("%s") is not a valid sound.',
+ $value));
+ }
+ }
+
+ protected function newDropTransactions($object, $value) {
+ return array();
+ }
+
+ protected function newDropEffects($value) {
+ $sound_icon = 'fa-volume-up';
+ $sound_color = 'blue';
+ $sound_name = self::getSoundName($value);
+
+ $content = pht(
+ 'Play sound %s.',
+ phutil_tag('strong', array(), $sound_name));
+
+ return array(
+ $this->newEffect()
+ ->setIcon($sound_icon)
+ ->setColor($sound_color)
+ ->setContent($content),
+ );
+ }
+
+ protected function getDefaultValue() {
+ return head_key(self::getSoundMap());
+ }
+
+ protected function getPHUIXControlType() {
+ return 'select';
+ }
+
+ protected function getPHUIXControlSpecification() {
+ $map = self::getSoundMap();
+ $map = ipull($map, 'name');
+
+ return array(
+ 'options' => $map,
+ 'order' => array_keys($map),
+ );
+ }
+
+ public function getRuleViewLabel() {
+ return pht('Play Sound');
+ }
+
+ public function getRuleViewDescription($value) {
+ $sound_name = self::getSoundName($value);
+
+ return pht(
+ 'Play sound %s.',
+ phutil_tag('strong', array(), $sound_name));
+ }
+
+ public function getRuleViewIcon($value) {
+ $sound_icon = 'fa-volume-up';
+ $sound_color = 'blue';
+
+ return id(new PHUIIconView())
+ ->setIcon($sound_icon, $sound_color);
+ }
+
+ private static function getSoundName($value) {
+ $map = self::getSoundMap();
+ $spec = idx($map, $value, array());
+ return idx($spec, 'name', $value);
+ }
+
+ private static function getSoundMap() {
+ return array(
+ 'bing' => array(
+ 'name' => pht('Bing'),
+ 'uri' => celerity_get_resource_uri('/rsrc/audio/basic/bing.mp3'),
+ ),
+ 'glass' => array(
+ 'name' => pht('Glass'),
+ 'uri' => celerity_get_resource_uri('/rsrc/audio/basic/ting.mp3'),
+ ),
+ );
+ }
+
+ public function getSoundEffects() {
+ $value = $this->getValue();
+
+ $map = self::getSoundMap();
+ $spec = idx($map, $value, array());
+
+ $uris = array();
+ if (isset($spec['uri'])) {
+ $uris[] = $spec['uri'];
+ }
+
+ return $uris;
+ }
+
+}
diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerRule.php
new file mode 100644
index 000000000..ae2b3ee09
--- /dev/null
+++ b/src/applications/project/trigger/PhabricatorProjectTriggerRule.php
@@ -0,0 +1,153 @@
+<?php
+
+abstract class PhabricatorProjectTriggerRule
+ extends Phobject {
+
+ private $record;
+ private $viewer;
+ private $column;
+ private $trigger;
+ private $object;
+
+ final public function getTriggerType() {
+ return $this->getPhobjectClassConstant('TRIGGERTYPE', 64);
+ }
+
+ final public static function getAllTriggerRules() {
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getTriggerType')
+ ->execute();
+ }
+
+ final public function setRecord(PhabricatorProjectTriggerRuleRecord $record) {
+ $value = $record->getValue();
+
+ $this->assertValidRuleValue($value);
+
+ $this->record = $record;
+ return $this;
+ }
+
+ final public function getRecord() {
+ return $this->record;
+ }
+
+ final protected function getValue() {
+ return $this->getRecord()->getValue();
+ }
+
+ abstract public function getSelectControlName();
+ abstract public function getRuleViewLabel();
+ abstract public function getRuleViewDescription($value);
+ abstract public function getRuleViewIcon($value);
+ abstract protected function assertValidRuleValue($value);
+ abstract protected function newDropTransactions($object, $value);
+ abstract protected function newDropEffects($value);
+ abstract protected function getDefaultValue();
+ abstract protected function getPHUIXControlType();
+ abstract protected function getPHUIXControlSpecification();
+
+ protected function isSelectableRule() {
+ return true;
+ }
+
+ protected function isValidRule() {
+ return true;
+ }
+
+ protected function newInvalidView() {
+ return null;
+ }
+
+ public function getSoundEffects() {
+ return array();
+ }
+
+ final public function getDropTransactions($object, $value) {
+ return $this->newDropTransactions($object, $value);
+ }
+
+ final public function setViewer(PhabricatorUser $viewer) {
+ $this->viewer = $viewer;
+ return $this;
+ }
+
+ final public function getViewer() {
+ return $this->viewer;
+ }
+
+ final public function setColumn(PhabricatorProjectColumn $column) {
+ $this->column = $column;
+ return $this;
+ }
+
+ final public function getColumn() {
+ return $this->column;
+ }
+
+ final public function setTrigger(PhabricatorProjectTrigger $trigger) {
+ $this->trigger = $trigger;
+ return $this;
+ }
+
+ final public function getTrigger() {
+ return $this->trigger;
+ }
+
+ final public function setObject(
+ PhabricatorApplicationTransactionInterface $object) {
+ $this->object = $object;
+ return $this;
+ }
+
+ final public function getObject() {
+ return $this->object;
+ }
+
+ final protected function newTransaction() {
+ return $this->getObject()->getApplicationTransactionTemplate();
+ }
+
+ final public function getDropEffects() {
+ return $this->newDropEffects($this->getValue());
+ }
+
+ final protected function newEffect() {
+ return id(new PhabricatorProjectDropEffect())
+ ->setIsTriggerEffect(true);
+ }
+
+ final public function toDictionary() {
+ $record = $this->getRecord();
+
+ $is_valid = $this->isValidRule();
+ if (!$is_valid) {
+ $invalid_view = hsprintf('%s', $this->newInvalidView());
+ } else {
+ $invalid_view = null;
+ }
+
+ return array(
+ 'type' => $record->getType(),
+ 'value' => $record->getValue(),
+ 'isValidRule' => $is_valid,
+ 'invalidView' => $invalid_view,
+ );
+ }
+
+ final public function newTemplate() {
+ return array(
+ 'type' => $this->getTriggerType(),
+ 'name' => $this->getSelectControlName(),
+ 'selectable' => $this->isSelectableRule(),
+ 'defaultValue' => $this->getDefaultValue(),
+ 'control' => array(
+ 'type' => $this->getPHUIXControlType(),
+ 'specification' => $this->getPHUIXControlSpecification(),
+ ),
+ );
+ }
+
+
+}
diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerRuleRecord.php b/src/applications/project/trigger/PhabricatorProjectTriggerRuleRecord.php
new file mode 100644
index 000000000..da36d9a4d
--- /dev/null
+++ b/src/applications/project/trigger/PhabricatorProjectTriggerRuleRecord.php
@@ -0,0 +1,27 @@
+<?php
+
+final class PhabricatorProjectTriggerRuleRecord
+ extends Phobject {
+
+ private $type;
+ private $value;
+
+ public function setType($type) {
+ $this->type = $type;
+ return $this;
+ }
+
+ public function getType() {
+ return $this->type;
+ }
+
+ public function setValue($value) {
+ $this->value = $value;
+ return $this;
+ }
+
+ public function getValue() {
+ return $this->value;
+ }
+
+}
diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php
new file mode 100644
index 000000000..925a369ba
--- /dev/null
+++ b/src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php
@@ -0,0 +1,71 @@
+<?php
+
+final class PhabricatorProjectTriggerUnknownRule
+ extends PhabricatorProjectTriggerRule {
+
+ const TRIGGERTYPE = 'unknown';
+
+ public function getSelectControlName() {
+ return pht('(Unknown Rule)');
+ }
+
+ protected function isSelectableRule() {
+ return false;
+ }
+
+ protected function assertValidRuleValue($value) {
+ return;
+ }
+
+ protected function newDropTransactions($object, $value) {
+ return array();
+ }
+
+ protected function newDropEffects($value) {
+ return array();
+ }
+
+ protected function isValidRule() {
+ return false;
+ }
+
+ protected function newInvalidView() {
+ return array(
+ id(new PHUIIconView())
+ ->setIcon('fa-exclamation-triangle yellow'),
+ ' ',
+ pht(
+ 'This is a trigger rule with a unknown type ("%s").',
+ $this->getRecord()->getType()),
+ );
+ }
+
+ protected function getDefaultValue() {
+ return null;
+ }
+
+ protected function getPHUIXControlType() {
+ return null;
+ }
+
+ protected function getPHUIXControlSpecification() {
+ return null;
+ }
+
+ public function getRuleViewLabel() {
+ return pht('Unknown Rule');
+ }
+
+ public function getRuleViewDescription($value) {
+ return pht(
+ 'This is an unknown rule of type "%s". An administrator may have '.
+ 'edited or removed an extension which implements this rule type.',
+ $this->getRecord()->getType());
+ }
+
+ public function getRuleViewIcon($value) {
+ return id(new PHUIIconView())
+ ->setIcon('fa-question-circle', 'yellow');
+ }
+
+}
diff --git a/src/applications/project/view/ProjectBoardTaskCard.php b/src/applications/project/view/ProjectBoardTaskCard.php
index 3a7016ca7..d102ac1b1 100644
--- a/src/applications/project/view/ProjectBoardTaskCard.php
+++ b/src/applications/project/view/ProjectBoardTaskCard.php
@@ -1,162 +1,186 @@
<?php
final class ProjectBoardTaskCard extends Phobject {
private $viewer;
private $projectHandles;
private $task;
private $owner;
+ private $showEditControls;
private $canEdit;
private $coverImageFile;
private $hideArchivedProjects;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setProjectHandles(array $handles) {
$this->projectHandles = $handles;
return $this;
}
public function getProjectHandles() {
return $this->projectHandles;
}
public function setCoverImageFile(PhabricatorFile $cover_image_file) {
$this->coverImageFile = $cover_image_file;
return $this;
}
public function getCoverImageFile() {
return $this->coverImageFile;
}
public function setHideArchivedProjects($hide_archived_projects) {
$this->hideArchivedProjects = $hide_archived_projects;
return $this;
}
public function getHideArchivedProjects() {
return $this->hideArchivedProjects;
}
public function setTask(ManiphestTask $task) {
$this->task = $task;
return $this;
}
public function getTask() {
return $this->task;
}
public function setOwner(PhabricatorObjectHandle $owner = null) {
$this->owner = $owner;
return $this;
}
public function getOwner() {
return $this->owner;
}
public function setCanEdit($can_edit) {
$this->canEdit = $can_edit;
return $this;
}
public function getCanEdit() {
return $this->canEdit;
}
+ public function setShowEditControls($show_edit_controls) {
+ $this->showEditControls = $show_edit_controls;
+ return $this;
+ }
+
+ public function getShowEditControls() {
+ return $this->showEditControls;
+ }
+
public function getItem() {
$task = $this->getTask();
$owner = $this->getOwner();
$can_edit = $this->getCanEdit();
$viewer = $this->getViewer();
$color_map = ManiphestTaskPriority::getColorMap();
$bar_color = idx($color_map, $task->getPriority(), 'grey');
$card = id(new PHUIObjectItemView())
->setObject($task)
->setUser($viewer)
- ->setObjectName('T'.$task->getID())
+ ->setObjectName($task->getMonogram())
->setHeader($task->getTitle())
- ->setGrippable($can_edit)
- ->setHref('/T'.$task->getID())
+ ->setHref($task->getURI())
->addSigil('project-card')
->setDisabled($task->isClosed())
- ->addAction(
- id(new PHUIListItemView())
- ->setName(pht('Edit'))
- ->setIcon('fa-pencil')
- ->addSigil('edit-project-card')
- ->setHref('/maniphest/task/edit/'.$task->getID().'/'))
->setBarColor($bar_color);
+ if ($this->getShowEditControls()) {
+ if ($can_edit) {
+ $card
+ ->addSigil('draggable-card')
+ ->addClass('draggable-card');
+ $edit_icon = 'fa-pencil';
+ } else {
+ $card
+ ->addClass('not-editable')
+ ->addClass('undraggable-card');
+ $edit_icon = 'fa-lock red';
+ }
+
+ $card->addAction(
+ id(new PHUIListItemView())
+ ->setName(pht('Edit'))
+ ->setIcon($edit_icon)
+ ->addSigil('edit-project-card')
+ ->setHref('/maniphest/task/edit/'.$task->getID().'/'));
+ }
+
if ($owner) {
$card->addHandleIcon($owner, $owner->getName());
}
$cover_file = $this->getCoverImageFile();
if ($cover_file) {
$card->setCoverImage($cover_file->getBestURI());
}
if (ManiphestTaskPoints::getIsEnabled()) {
$points = $task->getPoints();
if ($points !== null) {
$points_tag = id(new PHUITagView())
->setType(PHUITagView::TYPE_SHADE)
->setColor(PHUITagView::COLOR_GREY)
->setSlimShady(true)
->setName($points)
->addClass('phui-workcard-points');
$card->addAttribute($points_tag);
}
}
$subtype = $task->newSubtypeObject();
if ($subtype && $subtype->hasTagView()) {
$subtype_tag = $subtype->newTagView()
->setSlimShady(true);
$card->addAttribute($subtype_tag);
}
if ($task->isClosed()) {
$icon = ManiphestTaskStatus::getStatusIcon($task->getStatus());
$icon = id(new PHUIIconView())
->setIcon($icon.' grey');
$card->addAttribute($icon);
$card->setBarColor('grey');
}
$project_handles = $this->getProjectHandles();
// Remove any archived projects from the list.
if ($this->hideArchivedProjects) {
if ($project_handles) {
foreach ($project_handles as $key => $handle) {
if ($handle->getStatus() == PhabricatorObjectHandle::STATUS_CLOSED) {
unset($project_handles[$key]);
}
}
}
}
if ($project_handles) {
$project_handles = array_reverse($project_handles);
$tag_list = id(new PHUIHandleTagListView())
->setSlim(true)
->setHandles($project_handles);
$card->addAttribute($tag_list);
}
$card->addClass('phui-workcard');
return $card;
}
}
diff --git a/src/applications/project/xaction/column/PhabricatorProjectColumnLimitTransaction.php b/src/applications/project/xaction/column/PhabricatorProjectColumnLimitTransaction.php
new file mode 100644
index 000000000..8e91ccbe5
--- /dev/null
+++ b/src/applications/project/xaction/column/PhabricatorProjectColumnLimitTransaction.php
@@ -0,0 +1,63 @@
+<?php
+
+final class PhabricatorProjectColumnLimitTransaction
+ extends PhabricatorProjectColumnTransactionType {
+
+ const TRANSACTIONTYPE = 'project:col:limit';
+
+ public function generateOldValue($object) {
+ return $object->getPointLimit();
+ }
+
+ public function generateNewValue($object, $value) {
+ if (strlen($value)) {
+ return (int)$value;
+ } else {
+ return null;
+ }
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setPointLimit($value);
+ }
+
+ public function getTitle() {
+ $old = $this->getOldValue();
+ $new = $this->getNewValue();
+
+ if (!$old) {
+ return pht(
+ '%s set the point limit for this column to %s.',
+ $this->renderAuthor(),
+ $this->renderNewValue());
+ } else if (!$new) {
+ return pht(
+ '%s removed the point limit for this column.',
+ $this->renderAuthor());
+ } else {
+ return pht(
+ '%s changed the point limit for this column from %s to %s.',
+ $this->renderAuthor(),
+ $this->renderOldValue(),
+ $this->renderNewValue());
+ }
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+
+ foreach ($xactions as $xaction) {
+ $value = $xaction->getNewValue();
+ if (strlen($value) && !preg_match('/^\d+\z/', $value)) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Column point limit must either be empty or a nonnegative '.
+ 'integer.'),
+ $xaction);
+ }
+ }
+
+ return $errors;
+ }
+
+}
diff --git a/src/applications/project/xaction/column/PhabricatorProjectColumnNameTransaction.php b/src/applications/project/xaction/column/PhabricatorProjectColumnNameTransaction.php
new file mode 100644
index 000000000..bff54277d
--- /dev/null
+++ b/src/applications/project/xaction/column/PhabricatorProjectColumnNameTransaction.php
@@ -0,0 +1,66 @@
+<?php
+
+final class PhabricatorProjectColumnNameTransaction
+ extends PhabricatorProjectColumnTransactionType {
+
+ const TRANSACTIONTYPE = 'project:col:name';
+
+ public function generateOldValue($object) {
+ return $object->getName();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setName($value);
+ }
+
+ public function getTitle() {
+ $old = $this->getOldValue();
+ $new = $this->getNewValue();
+
+ if (!strlen($old)) {
+ return pht(
+ '%s named this column %s.',
+ $this->renderAuthor(),
+ $this->renderNewValue());
+ } else if (strlen($new)) {
+ return pht(
+ '%s renamed this column from %s to %s.',
+ $this->renderAuthor(),
+ $this->renderOldValue(),
+ $this->renderNewValue());
+ } else {
+ return pht(
+ '%s removed the custom name of this column.',
+ $this->renderAuthor());
+ }
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+
+ if ($this->isEmptyTextTransaction($object->getName(), $xactions)) {
+ // The default "Backlog" column is allowed to be unnamed, which
+ // means we use the default name.
+ if (!$object->isDefaultColumn()) {
+ $errors[] = $this->newRequiredError(
+ pht('Columns must have a name.'));
+ }
+ }
+
+ $max_length = $object->getColumnMaximumByteLength('name');
+ foreach ($xactions as $xaction) {
+ $new_value = $xaction->getNewValue();
+ $new_length = strlen($new_value);
+ if ($new_length > $max_length) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Column names must not be longer than %s characters.',
+ new PhutilNumber($max_length)),
+ $xaction);
+ }
+ }
+
+ return $errors;
+ }
+
+}
diff --git a/src/applications/project/xaction/column/PhabricatorProjectColumnStatusTransaction.php b/src/applications/project/xaction/column/PhabricatorProjectColumnStatusTransaction.php
new file mode 100644
index 000000000..7aab57c8e
--- /dev/null
+++ b/src/applications/project/xaction/column/PhabricatorProjectColumnStatusTransaction.php
@@ -0,0 +1,64 @@
+<?php
+
+final class PhabricatorProjectColumnStatusTransaction
+ extends PhabricatorProjectColumnTransactionType {
+
+ const TRANSACTIONTYPE = 'project:col:status';
+
+ public function generateOldValue($object) {
+ return $object->getStatus();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setStatus($value);
+ }
+
+ public function applyExternalEffects($object, $value) {
+ // Update the trigger usage index, which cares about whether columns are
+ // active or not.
+ $trigger_phid = $object->getTriggerPHID();
+ if ($trigger_phid) {
+ PhabricatorSearchWorker::queueDocumentForIndexing($trigger_phid);
+ }
+ }
+
+ public function getTitle() {
+ $new = $this->getNewValue();
+
+ switch ($new) {
+ case PhabricatorProjectColumn::STATUS_ACTIVE:
+ return pht(
+ '%s unhid this column.',
+ $this->renderAuthor());
+ case PhabricatorProjectColumn::STATUS_HIDDEN:
+ return pht(
+ '%s hid this column.',
+ $this->renderAuthor());
+ }
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+
+ $map = array(
+ PhabricatorProjectColumn::STATUS_ACTIVE,
+ PhabricatorProjectColumn::STATUS_HIDDEN,
+ );
+ $map = array_fuse($map);
+
+ foreach ($xactions as $xaction) {
+ $value = $xaction->getNewValue();
+ if (!isset($map[$value])) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Column status "%s" is unrecognized, valid statuses are: %s.',
+ $value,
+ implode(', ', array_keys($map))),
+ $xaction);
+ }
+ }
+
+ return $errors;
+ }
+
+}
diff --git a/src/applications/project/xaction/column/PhabricatorProjectColumnTransactionType.php b/src/applications/project/xaction/column/PhabricatorProjectColumnTransactionType.php
new file mode 100644
index 000000000..1473d3cab
--- /dev/null
+++ b/src/applications/project/xaction/column/PhabricatorProjectColumnTransactionType.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class PhabricatorProjectColumnTransactionType
+ extends PhabricatorModularTransactionType {}
diff --git a/src/applications/project/xaction/column/PhabricatorProjectColumnTriggerTransaction.php b/src/applications/project/xaction/column/PhabricatorProjectColumnTriggerTransaction.php
new file mode 100644
index 000000000..5339699de
--- /dev/null
+++ b/src/applications/project/xaction/column/PhabricatorProjectColumnTriggerTransaction.php
@@ -0,0 +1,97 @@
+<?php
+
+final class PhabricatorProjectColumnTriggerTransaction
+ extends PhabricatorProjectColumnTransactionType {
+
+ const TRANSACTIONTYPE = 'trigger';
+
+ public function generateOldValue($object) {
+ return $object->getTriggerPHID();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setTriggerPHID($value);
+ }
+
+ public function applyExternalEffects($object, $value) {
+ // After we change the trigger attached to a column, update the search
+ // indexes for the old and new triggers so we update the usage index.
+ $old = $this->getOldValue();
+ $new = $this->getNewValue();
+
+ $column_phids = array();
+ if ($old) {
+ $column_phids[] = $old;
+ }
+ if ($new) {
+ $column_phids[] = $new;
+ }
+
+ foreach ($column_phids as $phid) {
+ PhabricatorSearchWorker::queueDocumentForIndexing($phid);
+ }
+ }
+
+ public function getTitle() {
+ $old = $this->getOldValue();
+ $new = $this->getNewValue();
+
+ if (!$old) {
+ return pht(
+ '%s set the column trigger to %s.',
+ $this->renderAuthor(),
+ $this->renderNewHandle());
+ } else if (!$new) {
+ return pht(
+ '%s removed the trigger for this column (was %s).',
+ $this->renderAuthor(),
+ $this->renderOldHandle());
+ } else {
+ return pht(
+ '%s changed the trigger for this column from %s to %s.',
+ $this->renderAuthor(),
+ $this->renderOldHandle(),
+ $this->renderNewHandle());
+ }
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $actor = $this->getActor();
+ $errors = array();
+
+ foreach ($xactions as $xaction) {
+ $trigger_phid = $xaction->getNewValue();
+
+ // You can always remove a trigger.
+ if (!$trigger_phid) {
+ continue;
+ }
+
+ // You can't put a trigger on a column that can't have triggers, like
+ // a backlog column or a proxy column.
+ if (!$object->canHaveTrigger()) {
+ $errors[] = $this->newInvalidError(
+ pht('This column can not have a trigger.'),
+ $xaction);
+ continue;
+ }
+
+ $trigger = id(new PhabricatorProjectTriggerQuery())
+ ->setViewer($actor)
+ ->withPHIDs(array($trigger_phid))
+ ->execute();
+ if (!$trigger) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Trigger "%s" is not a valid trigger, or you do not have '.
+ 'permission to view it.',
+ $trigger_phid),
+ $xaction);
+ continue;
+ }
+ }
+
+ return $errors;
+ }
+
+}
diff --git a/src/applications/project/xaction/trigger/PhabricatorProjectTriggerNameTransaction.php b/src/applications/project/xaction/trigger/PhabricatorProjectTriggerNameTransaction.php
new file mode 100644
index 000000000..91a1be6bd
--- /dev/null
+++ b/src/applications/project/xaction/trigger/PhabricatorProjectTriggerNameTransaction.php
@@ -0,0 +1,58 @@
+<?php
+
+final class PhabricatorProjectTriggerNameTransaction
+ extends PhabricatorProjectTriggerTransactionType {
+
+ const TRANSACTIONTYPE = 'name';
+
+ public function generateOldValue($object) {
+ return $object->getName();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setName($value);
+ }
+
+ public function getTitle() {
+ $old = $this->getOldValue();
+ $new = $this->getNewValue();
+
+ if (strlen($old) && strlen($new)) {
+ return pht(
+ '%s renamed this trigger from %s to %s.',
+ $this->renderAuthor(),
+ $this->renderOldValue(),
+ $this->renderNewValue());
+ } else if (strlen($new)) {
+ return pht(
+ '%s named this trigger %s.',
+ $this->renderAuthor(),
+ $this->renderNewValue());
+ } else {
+ return pht(
+ '%s stripped the name %s from this trigger.',
+ $this->renderAuthor(),
+ $this->renderOldValue());
+ }
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+
+ $max_length = $object->getColumnMaximumByteLength('name');
+ foreach ($xactions as $xaction) {
+ $new_value = $xaction->getNewValue();
+ $new_length = strlen($new_value);
+ if ($new_length > $max_length) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Trigger names must not be longer than %s characters.',
+ new PhutilNumber($max_length)),
+ $xaction);
+ }
+ }
+
+ return $errors;
+ }
+
+}
diff --git a/src/applications/project/xaction/trigger/PhabricatorProjectTriggerRulesetTransaction.php b/src/applications/project/xaction/trigger/PhabricatorProjectTriggerRulesetTransaction.php
new file mode 100644
index 000000000..59c846bec
--- /dev/null
+++ b/src/applications/project/xaction/trigger/PhabricatorProjectTriggerRulesetTransaction.php
@@ -0,0 +1,65 @@
+<?php
+
+final class PhabricatorProjectTriggerRulesetTransaction
+ extends PhabricatorProjectTriggerTransactionType {
+
+ const TRANSACTIONTYPE = 'ruleset';
+
+ public function generateOldValue($object) {
+ return $object->getRuleset();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setRuleset($value);
+ }
+
+ public function getTitle() {
+ return pht(
+ '%s updated the ruleset for this trigger.',
+ $this->renderAuthor());
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+
+ foreach ($xactions as $xaction) {
+ $ruleset = $xaction->getNewValue();
+
+ try {
+ PhabricatorProjectTrigger::newTriggerRulesFromRuleSpecifications(
+ $ruleset,
+ $allow_invalid = false);
+ } catch (PhabricatorProjectTriggerCorruptionException $ex) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Ruleset specification is not valid. %s',
+ $ex->getMessage()),
+ $xaction);
+ continue;
+ }
+ }
+
+ return $errors;
+ }
+
+ public function hasChangeDetailView() {
+ return true;
+ }
+
+ public function newChangeDetailView() {
+ $viewer = $this->getViewer();
+
+ $old = $this->getOldValue();
+ $new = $this->getNewValue();
+
+ $json = new PhutilJSON();
+ $old_json = $json->encodeAsList($old);
+ $new_json = $json->encodeAsList($new);
+
+ return id(new PhabricatorApplicationTransactionTextDiffDetailView())
+ ->setViewer($viewer)
+ ->setOldText($old_json)
+ ->setNewText($new_json);
+ }
+
+}
diff --git a/src/applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php b/src/applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php
new file mode 100644
index 000000000..30222e1e2
--- /dev/null
+++ b/src/applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class PhabricatorProjectTriggerTransactionType
+ extends PhabricatorModularTransactionType {}
diff --git a/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php b/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php
index ffd638828..1835e6f7f 100644
--- a/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php
+++ b/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php
@@ -1,102 +1,102 @@
<?php
// TODO: After T2222, this is likely unreachable?
final class ReleephRequestDifferentialCreateController
extends ReleephController {
private $revision;
public function handleRequest(AphrontRequest $request) {
$revision_id = $request->getURIData('diffRevID');
$viewer = $request->getViewer();
$diff_rev = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($revision_id))
->executeOne();
if (!$diff_rev) {
return new Aphront404Response();
}
$this->revision = $diff_rev;
$repository = $this->revision->getRepository();
$projects = id(new ReleephProject())->loadAllWhere(
'repositoryPHID = %s AND isActive = 1',
$repository->getPHID());
if (!$projects) {
throw new Exception(
pht(
"%s belongs to the '%s' repository, ".
"which is not part of any Releeph project!",
'D'.$this->revision->getID(),
$repository->getMonogram()));
}
$branches = id(new ReleephBranch())->loadAllWhere(
'releephProjectID IN (%Ld) AND isActive = 1',
mpull($projects, 'getID'));
if (!$branches) {
throw new Exception(pht(
'%s could be in the Releeph project(s) %s, '.
'but this project / none of these projects have open branches.',
'D'.$this->revision->getID(),
implode(', ', mpull($projects, 'getName'))));
}
if (count($branches) === 1) {
return id(new AphrontRedirectResponse())
->setURI($this->buildReleephRequestURI(head($branches)));
}
$projects = msort(
mpull($projects, null, 'getID'),
'getName');
$branch_groups = mgroup($branches, 'getReleephProjectID');
require_celerity_resource('releeph-request-differential-create-dialog');
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('Choose Releeph Branch'))
->setClass('releeph-request-differential-create-dialog')
->addCancelButton('/D'.$request->getStr('D'));
$dialog->appendChild(
pht(
'This differential revision changes code that is associated '.
'with multiple Releeph branches. Please select the branch '.
'where you would like this code to be picked.'));
foreach ($branch_groups as $project_id => $branches) {
$project = idx($projects, $project_id);
$dialog->appendChild(
phutil_tag(
'h1',
array(),
$project->getName()));
$branches = msort($branches, 'getBasename');
foreach ($branches as $branch) {
$uri = $this->buildReleephRequestURI($branch);
$dialog->appendChild(
phutil_tag(
'a',
array(
'href' => $uri,
),
$branch->getDisplayNameWithDetail()));
}
}
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
private function buildReleephRequestURI(ReleephBranch $branch) {
$uri = $branch->getURI('request/');
return id(new PhutilURI($uri))
- ->setQueryParam('D', $this->revision->getID());
+ ->replaceQueryParam('D', $this->revision->getID());
}
}
diff --git a/src/applications/releeph/query/ReleephProductQuery.php b/src/applications/releeph/query/ReleephProductQuery.php
index c03995037..118b9919a 100644
--- a/src/applications/releeph/query/ReleephProductQuery.php
+++ b/src/applications/releeph/query/ReleephProductQuery.php
@@ -1,146 +1,144 @@
<?php
final class ReleephProductQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $active;
private $ids;
private $phids;
private $repositoryPHIDs;
const ORDER_ID = 'order-id';
const ORDER_NAME = 'order-name';
public function withActive($active) {
$this->active = $active;
return $this;
}
public function setOrder($order) {
switch ($order) {
case self::ORDER_ID:
$this->setOrderVector(array('id'));
break;
case self::ORDER_NAME:
$this->setOrderVector(array('name'));
break;
default:
throw new Exception(pht('Order "%s" not supported.', $order));
}
return $this;
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withRepositoryPHIDs(array $repository_phids) {
$this->repositoryPHIDs = $repository_phids;
return $this;
}
protected function loadPage() {
$table = new ReleephProject();
$conn_r = $table->establishConnection('r');
$rows = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($rows);
}
protected function willFilterPage(array $projects) {
assert_instances_of($projects, 'ReleephProject');
$repository_phids = mpull($projects, 'getRepositoryPHID');
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withPHIDs($repository_phids)
->execute();
$repositories = mpull($repositories, null, 'getPHID');
foreach ($projects as $key => $project) {
$repo = idx($repositories, $project->getRepositoryPHID());
if (!$repo) {
unset($projects[$key]);
continue;
}
$project->attachRepository($repo);
}
return $projects;
}
protected function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
if ($this->active !== null) {
$where[] = qsprintf(
$conn,
'isActive = %d',
(int)$this->active);
}
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ls)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->repositoryPHIDs !== null) {
$where[] = qsprintf(
$conn,
'repositoryPHID IN (%Ls)',
$this->repositoryPHIDs);
}
$where[] = $this->buildPagingClause($conn);
return $this->formatWhereClause($conn, $where);
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'column' => 'name',
'unique' => true,
'reverse' => true,
'type' => 'string',
),
);
}
- protected function getPagingValueMap($cursor, array $keys) {
- $product = $this->loadCursorObject($cursor);
-
+ protected function newPagingMapFromPartialObject($object) {
return array(
- 'id' => $product->getID(),
- 'name' => $product->getName(),
+ 'id' => (int)$object->getID(),
+ 'name' => $object->getName(),
);
}
public function getQueryApplicationClass() {
return 'PhabricatorReleephApplication';
}
}
diff --git a/src/applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php b/src/applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php
index ba78b0fe7..6ca67257c 100644
--- a/src/applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php
+++ b/src/applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php
@@ -1,85 +1,89 @@
<?php
final class PhabricatorRepositoryRepositoryPHIDType
extends PhabricatorPHIDType {
const TYPECONST = 'REPO';
public function getTypeName() {
return pht('Repository');
}
public function getTypeIcon() {
return 'fa-code';
}
public function newObject() {
return new PhabricatorRepository();
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new PhabricatorRepositoryQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$repository = $objects[$phid];
$monogram = $repository->getMonogram();
$name = $repository->getName();
$uri = $repository->getURI();
$handle
->setName($monogram)
->setFullName("{$monogram} {$name}")
->setURI($uri)
->setMailStampName($monogram);
+
+ if ($repository->getStatus() !== PhabricatorRepository::STATUS_ACTIVE) {
+ $handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED);
+ }
}
}
public function canLoadNamedObject($name) {
return preg_match('/^(r[A-Z]+|R[1-9]\d*)\z/', $name);
}
public function loadNamedObjects(
PhabricatorObjectQuery $query,
array $names) {
$results = array();
$id_map = array();
foreach ($names as $key => $name) {
$id = substr($name, 1);
$id_map[$id][] = $name;
$names[$key] = substr($name, 1);
}
$query = id(new PhabricatorRepositoryQuery())
->setViewer($query->getViewer())
->withIdentifiers($names);
if ($query->execute()) {
$objects = $query->getIdentifierMap();
foreach ($objects as $key => $object) {
foreach (idx($id_map, $key, array()) as $name) {
$results[$name] = $object;
}
}
return $results;
} else {
return array();
}
}
}
diff --git a/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php b/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php
index ef038f045..c64b1a296 100644
--- a/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php
+++ b/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php
@@ -1,154 +1,131 @@
<?php
final class PhabricatorRepositoryIdentityQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $identityNames;
private $emailAddress;
private $assigneePHIDs;
private $identityNameLike;
private $hasEffectivePHID;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withIdentityNames(array $names) {
$this->identityNames = $names;
return $this;
}
public function withIdentityNameLike($name_like) {
$this->identityNameLike = $name_like;
return $this;
}
public function withEmailAddress($address) {
$this->emailAddress = $address;
return $this;
}
public function withAssigneePHIDs(array $assignees) {
$this->assigneePHIDs = $assignees;
return $this;
}
public function withHasEffectivePHID($has_effective_phid) {
$this->hasEffectivePHID = $has_effective_phid;
return $this;
}
public function newResultObject() {
return new PhabricatorRepositoryIdentity();
}
protected function getPrimaryTableAlias() {
return 'repository_identity';
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'repository_identity.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'repository_identity.phid IN (%Ls)',
$this->phids);
}
if ($this->assigneePHIDs !== null) {
$where[] = qsprintf(
$conn,
'repository_identity.currentEffectiveUserPHID IN (%Ls)',
$this->assigneePHIDs);
}
if ($this->hasEffectivePHID !== null) {
if ($this->hasEffectivePHID) {
$where[] = qsprintf(
$conn,
'repository_identity.currentEffectiveUserPHID IS NOT NULL');
} else {
$where[] = qsprintf(
$conn,
'repository_identity.currentEffectiveUserPHID IS NULL');
}
}
if ($this->identityNames !== null) {
$name_hashes = array();
foreach ($this->identityNames as $name) {
$name_hashes[] = PhabricatorHash::digestForIndex($name);
}
$where[] = qsprintf(
$conn,
'repository_identity.identityNameHash IN (%Ls)',
$name_hashes);
}
if ($this->emailAddress !== null) {
$identity_style = "<{$this->emailAddress}>";
$where[] = qsprintf(
$conn,
'repository_identity.identityNameRaw LIKE %<',
$identity_style);
}
if ($this->identityNameLike != null) {
$where[] = qsprintf(
$conn,
'repository_identity.identityNameRaw LIKE %~',
$this->identityNameLike);
}
return $where;
}
- protected function didFilterPage(array $identities) {
- $user_ids = array_filter(
- mpull($identities, 'getCurrentEffectiveUserPHID', 'getID'));
- if (!$user_ids) {
- return $identities;
- }
-
- $users = id(new PhabricatorPeopleQuery())
- ->withPHIDs($user_ids)
- ->setViewer($this->getViewer())
- ->execute();
- $users = mpull($users, null, 'getPHID');
-
- foreach ($identities as $identity) {
- if ($identity->hasEffectiveUser()) {
- $user = idx($users, $identity->getCurrentEffectiveUserPHID());
- $identity->attachEffectiveUser($user);
- }
- }
-
- return $identities;
- }
-
public function getQueryApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
}
diff --git a/src/applications/repository/query/PhabricatorRepositoryQuery.php b/src/applications/repository/query/PhabricatorRepositoryQuery.php
index 9d1a8ce57..60dd6d0af 100644
--- a/src/applications/repository/query/PhabricatorRepositoryQuery.php
+++ b/src/applications/repository/query/PhabricatorRepositoryQuery.php
@@ -1,887 +1,862 @@
<?php
final class PhabricatorRepositoryQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $authors; // c4science custo
private $without_authors; // c4science custo
private $ids;
private $phids;
private $without_phids; // c4science custo
private $callsigns;
private $types;
private $uuids;
private $uris;
private $datasourceQuery;
private $slugs;
private $almanacServicePHIDs;
private $limitCommitCounts; // c4science custo
private $canpush; // c4science custo
private $canview; // c4science custo
private $canviewpublic; // c4science custo
private $numericIdentifiers;
private $callsignIdentifiers;
private $phidIdentifiers;
private $monogramIdentifiers;
private $slugIdentifiers;
private $identifierMap;
const STATUS_OPEN = 'status-open';
const STATUS_CLOSED = 'status-closed';
const STATUS_ALL = 'status-all';
private $status = self::STATUS_ALL;
const HOSTED_PHABRICATOR = 'hosted-phab';
const HOSTED_REMOTE = 'hosted-remote';
const HOSTED_ALL = 'hosted-all';
private $hosted = self::HOSTED_ALL;
private $needMostRecentCommits;
private $needCommitCounts;
private $needProjectPHIDs;
private $needURIs;
private $needProfileImage;
// c4science custo
public function withCanPush() {
$this->canpush = true;
return $this;
}
// c4science custo
public function withCanView() {
$this->canview = true;
return $this;
}
// c4science custo
public function withCanViewPublic() {
$this->canviewpublic = true;
return $this;
}
// c4science custo
public function withAuthors(array $authors) {
$this->authors = $authors;
return $this;
}
// c4science custo
public function withoutAuthors(array $authors) {
$this->without_authors = $authors;
return $this;
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
// c4science customization
public function withoutPHIDs(array $phids) {
$this->without_phids = $phids;
return $this;
}
public function withCallsigns(array $callsigns) {
$this->callsigns = $callsigns;
return $this;
}
public function withIdentifiers(array $identifiers) {
$identifiers = array_fuse($identifiers);
$ids = array();
$callsigns = array();
$phids = array();
$monograms = array();
$slugs = array();
foreach ($identifiers as $identifier) {
if (ctype_digit((string)$identifier)) {
$ids[$identifier] = $identifier;
continue;
}
if (preg_match('/^(r[A-Z]+|R[1-9]\d*)\z/', $identifier)) {
$monograms[$identifier] = $identifier;
continue;
}
$repository_type = PhabricatorRepositoryRepositoryPHIDType::TYPECONST;
if (phid_get_type($identifier) === $repository_type) {
$phids[$identifier] = $identifier;
continue;
}
if (preg_match('/^[A-Z]+\z/', $identifier)) {
$callsigns[$identifier] = $identifier;
continue;
}
$slugs[$identifier] = $identifier;
}
$this->numericIdentifiers = $ids;
$this->callsignIdentifiers = $callsigns;
$this->phidIdentifiers = $phids;
$this->monogramIdentifiers = $monograms;
$this->slugIdentifiers = $slugs;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withHosted($hosted) {
$this->hosted = $hosted;
return $this;
}
public function withTypes(array $types) {
$this->types = $types;
return $this;
}
public function withUUIDs(array $uuids) {
$this->uuids = $uuids;
return $this;
}
public function withURIs(array $uris) {
$this->uris = $uris;
return $this;
}
public function withDatasourceQuery($query) {
$this->datasourceQuery = $query;
return $this;
}
public function withSlugs(array $slugs) {
$this->slugs = $slugs;
return $this;
}
public function withAlmanacServicePHIDs(array $phids) {
$this->almanacServicePHIDs = $phids;
return $this;
}
public function needCommitCounts($need_counts) {
$this->needCommitCounts = $need_counts;
return $this;
}
public function needMostRecentCommits($need_commits) {
$this->needMostRecentCommits = $need_commits;
return $this;
}
public function needProjectPHIDs($need_phids) {
$this->needProjectPHIDs = $need_phids;
return $this;
}
public function needURIs($need_uris) {
$this->needURIs = $need_uris;
return $this;
}
public function needProfileImage($need) {
$this->needProfileImage = $need;
return $this;
}
// c4science customization
public function limitCommitCounts($limit) {
$this->needCommitCounts(true);
$this->limitCommitCounts = $limit;
return $this;
}
public function getBuiltinOrders() {
return array(
'committed' => array(
'vector' => array('committed', 'id'),
'name' => pht('Most Recent Commit'),
),
'name' => array(
'vector' => array('name', 'id'),
'name' => pht('Name'),
),
'callsign' => array(
'vector' => array('callsign'),
'name' => pht('Callsign'),
),
'size' => array(
'vector' => array('size', 'id'),
'name' => pht('Size'),
),
) + parent::getBuiltinOrders();
}
public function getIdentifierMap() {
if ($this->identifierMap === null) {
throw new PhutilInvalidStateException('execute');
}
return $this->identifierMap;
}
protected function willExecute() {
$this->identifierMap = array();
}
public function newResultObject() {
return new PhabricatorRepository();
}
protected function loadPage() {
$table = $this->newResultObject();
$data = $this->loadStandardPageRows($table);
$repositories = $table->loadAllFromArray($data);
if ($this->needCommitCounts) {
$sizes = ipull($data, 'size', 'id');
foreach ($repositories as $id => $repository) {
$repository->attachCommitCount(nonempty($sizes[$id], 0));
}
}
if ($this->needMostRecentCommits) {
$commit_ids = ipull($data, 'lastCommitID', 'id');
$commit_ids = array_filter($commit_ids);
if ($commit_ids) {
$commits = id(new DiffusionCommitQuery())
->setViewer($this->getViewer())
->withIDs($commit_ids)
->execute();
} else {
$commits = array();
}
foreach ($repositories as $id => $repository) {
$commit = null;
if (idx($commit_ids, $id)) {
$commit = idx($commits, $commit_ids[$id]);
}
$repository->attachMostRecentCommit($commit);
}
}
return $repositories;
}
protected function willFilterPage(array $repositories) {
assert_instances_of($repositories, 'PhabricatorRepository');
// TODO: Denormalize repository status into the PhabricatorRepository
// table so we can do this filtering in the database.
foreach ($repositories as $key => $repo) {
$status = $this->status;
switch ($status) {
case self::STATUS_OPEN:
if (!$repo->isTracked()) {
unset($repositories[$key]);
}
break;
case self::STATUS_CLOSED:
if ($repo->isTracked()) {
unset($repositories[$key]);
}
break;
case self::STATUS_ALL:
break;
default:
throw new Exception("Unknown status '{$status}'!");
}
// TODO: This should also be denormalized.
$hosted = $this->hosted;
switch ($hosted) {
case self::HOSTED_PHABRICATOR:
if (!$repo->isHosted()) {
unset($repositories[$key]);
}
break;
case self::HOSTED_REMOTE:
if ($repo->isHosted()) {
unset($repositories[$key]);
}
break;
case self::HOSTED_ALL:
break;
default:
throw new Exception(pht("Unknown hosted failed '%s'!", $hosted));
}
}
// Build the identifierMap
if ($this->numericIdentifiers) {
foreach ($this->numericIdentifiers as $id) {
if (isset($repositories[$id])) {
$this->identifierMap[$id] = $repositories[$id];
}
}
}
if ($this->callsignIdentifiers) {
$repository_callsigns = mpull($repositories, null, 'getCallsign');
foreach ($this->callsignIdentifiers as $callsign) {
if (isset($repository_callsigns[$callsign])) {
$this->identifierMap[$callsign] = $repository_callsigns[$callsign];
}
}
}
if ($this->phidIdentifiers) {
$repository_phids = mpull($repositories, null, 'getPHID');
foreach ($this->phidIdentifiers as $phid) {
if (isset($repository_phids[$phid])) {
$this->identifierMap[$phid] = $repository_phids[$phid];
}
}
}
if ($this->monogramIdentifiers) {
$monogram_map = array();
foreach ($repositories as $repository) {
foreach ($repository->getAllMonograms() as $monogram) {
$monogram_map[$monogram] = $repository;
}
}
foreach ($this->monogramIdentifiers as $monogram) {
if (isset($monogram_map[$monogram])) {
$this->identifierMap[$monogram] = $monogram_map[$monogram];
}
}
}
if ($this->slugIdentifiers) {
$slug_map = array();
foreach ($repositories as $repository) {
$slug = $repository->getRepositorySlug();
if ($slug === null) {
continue;
}
$normal = phutil_utf8_strtolower($slug);
$slug_map[$normal] = $repository;
}
foreach ($this->slugIdentifiers as $slug) {
$normal = phutil_utf8_strtolower($slug);
if (isset($slug_map[$normal])) {
$this->identifierMap[$slug] = $slug_map[$normal];
}
}
}
return $repositories;
}
protected function didFilterPage(array $repositories) {
if ($this->needProjectPHIDs) {
$type_project = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(mpull($repositories, 'getPHID'))
->withEdgeTypes(array($type_project));
$edge_query->execute();
foreach ($repositories as $repository) {
$project_phids = $edge_query->getDestinationPHIDs(
array(
$repository->getPHID(),
));
$repository->attachProjectPHIDs($project_phids);
}
}
$viewer = $this->getViewer();
if ($this->needURIs) {
$uris = id(new PhabricatorRepositoryURIQuery())
->setViewer($viewer)
->withRepositories($repositories)
->execute();
$uri_groups = mgroup($uris, 'getRepositoryPHID');
foreach ($repositories as $repository) {
$repository_uris = idx($uri_groups, $repository->getPHID(), array());
$repository->attachURIs($repository_uris);
}
}
if ($this->needProfileImage) {
$default = null;
$file_phids = mpull($repositories, 'getProfileImagePHID');
$file_phids = array_filter($file_phids);
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
} else {
$files = array();
}
foreach ($repositories as $repository) {
$file = idx($files, $repository->getProfileImagePHID());
if (!$file) {
if (!$default) {
$default = PhabricatorFile::loadBuiltin(
$this->getViewer(),
'repo/code.png');
}
$file = $default;
}
$repository->attachProfileImageFile($file);
}
}
return $repositories;
}
protected function getPrimaryTableAlias() {
return 'r';
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'committed' => array(
'table' => 's',
'column' => 'epoch',
'type' => 'int',
'null' => 'tail',
),
'callsign' => array(
'table' => 'r',
'column' => 'callsign',
'type' => 'string',
'unique' => true,
'reverse' => true,
'null' => 'tail',
),
'name' => array(
'table' => 'r',
'column' => 'name',
'type' => 'string',
'reverse' => true,
),
'size' => array(
'table' => 's',
'column' => 'size',
'type' => 'int',
'null' => 'tail',
),
);
}
- protected function willExecuteCursorQuery(
- PhabricatorCursorPagedPolicyAwareQuery $query) {
- $vector = $this->getOrderVector();
+ protected function newPagingMapFromCursorObject(
+ PhabricatorQueryCursor $cursor,
+ array $keys) {
- if ($vector->containsKey('committed')) {
- $query->needMostRecentCommits(true);
- }
-
- if ($vector->containsKey('size')) {
- $query->needCommitCounts(true);
- }
- }
-
- protected function getPagingValueMap($cursor, array $keys) {
- $repository = $this->loadCursorObject($cursor);
+ $repository = $cursor->getObject();
$map = array(
- 'id' => $repository->getID(),
+ 'id' => (int)$repository->getID(),
'callsign' => $repository->getCallsign(),
'name' => $repository->getName(),
);
- foreach ($keys as $key) {
- switch ($key) {
- case 'committed':
- $commit = $repository->getMostRecentCommit();
- if ($commit) {
- $map[$key] = $commit->getEpoch();
- } else {
- $map[$key] = null;
- }
- break;
- case 'size':
- $count = $repository->getCommitCount();
- if ($count) {
- $map[$key] = $count;
- } else {
- $map[$key] = null;
- }
- break;
- }
+ if (isset($keys['committed'])) {
+ $map['committed'] = $cursor->getRawRowProperty('epoch');
+ }
+
+ if (isset($keys['size'])) {
+ $map['size'] = $cursor->getRawRowProperty('size');
}
return $map;
}
protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
$parts = parent::buildSelectClauseParts($conn);
- $parts[] = qsprintf($conn, 'r.*');
-
if ($this->shouldJoinSummaryTable()) {
$parts[] = qsprintf($conn, 's.*');
}
return $parts;
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->shouldJoinSummaryTable()) {
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T s ON r.id = s.repositoryID',
PhabricatorRepository::TABLE_SUMMARY);
}
if ($this->shouldJoinURITable()) {
$joins[] = qsprintf(
$conn,
'LEFT JOIN %R uri ON r.phid = uri.repositoryPHID',
new PhabricatorRepositoryURIIndex());
}
// c4science custo
if($this->shouldJoinTransactionTable()) {
$joins[] = qsprintf(
$conn,
'LEFT JOIN repository_transaction t ON '
. 'r.phid = t.objectPHID and transactionType = %s',
PhabricatorTransactions::TYPE_CREATE);
}
// c4science custo
if($this->canview) {
// PLCY
$joins[] = qsprintf(
$conn,
'LEFT JOIN phabricator_policy.policy pv ON r.viewPolicy = pv.phid');
// PROJ
$joins[] = qsprintf(
$conn,
'LEFT JOIN phabricator_project.edge ev ON r.viewPolicy = ev.src '
. 'AND ev.type=%s',
PhabricatorProjectProjectHasMemberEdgeType::EDGECONST);
}
// c4science custo
if($this->canpush) {
// PLCY
$joins[] = qsprintf(
$conn,
'LEFT JOIN phabricator_policy.policy pp ON r.pushPolicy = pp.phid');
// PROJ
$joins[] = qsprintf(
$conn,
'LEFT JOIN phabricator_project.edge ep ON r.pushPolicy = ep.src '
. 'AND ep.type=%s',
PhabricatorProjectProjectHasMemberEdgeType::EDGECONST);
}
return $joins;
}
protected function shouldGroupQueryResultRows() {
if ($this->shouldJoinURITable()) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
private function shouldJoinURITable() {
return ($this->uris !== null);
}
// c4science customization
private function shouldJoinTransactionTable() {
return ($this->authors !== null
|| $this->without_authors !== null
|| $this->canview
|| $this->canpush);
}
private function shouldJoinSummaryTable() {
if ($this->needCommitCounts) {
return true;
}
if ($this->needMostRecentCommits) {
return true;
}
$vector = $this->getOrderVector();
if ($vector->containsKey('committed')) {
return true;
}
if ($vector->containsKey('size')) {
return true;
}
return false;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'r.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'r.phid IN (%Ls)',
$this->phids);
}
// C4science customization
if ($this->without_phids !== null) {
$where[] = qsprintf(
$conn,
'r.phid NOT IN (%Ls)',
$this->without_phids);
}
// C4science customization
if ($this->without_authors !== null) {
$where[] = qsprintf(
$conn,
't.authorPHID NOT IN (%Ls)',
$this->without_authors);
}
// C4science customization
if ($this->authors !== null) {
$where[] = qsprintf(
$conn,
't.authorPHID IN (%Ls)',
$this->authors);
}
// c4science custo
if($this->canviewpublic) {
$where[] = qsprintf(
$conn,
'r.viewPolicy = "public"');
}
// c4science customization
if($this->canview) {
$viewer = $this->getViewer()->getPHID();
$where[] = qsprintf(
$conn,
'(ev.dst=%s OR pv.rules LIKE %~ OR t.authorPHID=%s'
. ' OR r.viewPolicy in ("public", "users"))',
$viewer, $viewer, $viewer);
}
// c4science customization
if($this->canpush) {
$viewer = $this->getViewer()->getPHID();
// This is a rough check, the rule can also be deny to thes user
// and this doesn't take into account Projects in the policy.
// The policy aware query will take care of filtering after though.
$where[] = qsprintf(
$conn,
'(ep.dst=%s OR pp.rules LIKE %~)',
$viewer, $viewer);
}
if ($this->callsigns !== null) {
$where[] = qsprintf(
$conn,
'r.callsign IN (%Ls)',
$this->callsigns);
}
// c4science customization
if($this->needCommitCounts && $this->limitCommitCounts > 0) {
$where[] = qsprintf(
$conn,
's.size >= %d',
$this->limitCommitCounts);
}
if ($this->numericIdentifiers ||
$this->callsignIdentifiers ||
$this->phidIdentifiers ||
$this->monogramIdentifiers ||
$this->slugIdentifiers) {
$identifier_clause = array();
if ($this->numericIdentifiers) {
$identifier_clause[] = qsprintf(
$conn,
'r.id IN (%Ld)',
$this->numericIdentifiers);
}
if ($this->callsignIdentifiers) {
$identifier_clause[] = qsprintf(
$conn,
'r.callsign IN (%Ls)',
$this->callsignIdentifiers);
}
if ($this->phidIdentifiers) {
$identifier_clause[] = qsprintf(
$conn,
'r.phid IN (%Ls)',
$this->phidIdentifiers);
}
if ($this->monogramIdentifiers) {
$monogram_callsigns = array();
$monogram_ids = array();
foreach ($this->monogramIdentifiers as $identifier) {
if ($identifier[0] == 'r') {
$monogram_callsigns[] = substr($identifier, 1);
} else {
$monogram_ids[] = substr($identifier, 1);
}
}
if ($monogram_ids) {
$identifier_clause[] = qsprintf(
$conn,
'r.id IN (%Ld)',
$monogram_ids);
}
if ($monogram_callsigns) {
$identifier_clause[] = qsprintf(
$conn,
'r.callsign IN (%Ls)',
$monogram_callsigns);
}
}
if ($this->slugIdentifiers) {
$identifier_clause[] = qsprintf(
$conn,
'r.repositorySlug IN (%Ls)',
$this->slugIdentifiers);
}
$where[] = qsprintf($conn, '%LO', $identifier_clause);
}
if ($this->types) {
$where[] = qsprintf(
$conn,
'r.versionControlSystem IN (%Ls)',
$this->types);
}
if ($this->uuids) {
$where[] = qsprintf(
$conn,
'r.uuid IN (%Ls)',
$this->uuids);
}
if (strlen($this->datasourceQuery)) {
// This handles having "rP" match callsigns starting with "P...".
$query = trim($this->datasourceQuery);
if (preg_match('/^r/', $query)) {
$callsign = substr($query, 1);
} else {
$callsign = $query;
}
$where[] = qsprintf(
$conn,
'r.name LIKE %> OR r.callsign LIKE %> OR r.repositorySlug LIKE %>',
$query,
$callsign,
$query);
}
if ($this->slugs !== null) {
$where[] = qsprintf(
$conn,
'r.repositorySlug IN (%Ls)',
$this->slugs);
}
if ($this->uris !== null) {
$try_uris = $this->getNormalizedURIs();
$try_uris = array_fuse($try_uris);
$where[] = qsprintf(
$conn,
'uri.repositoryURI IN (%Ls)',
$try_uris);
}
if ($this->almanacServicePHIDs !== null) {
$where[] = qsprintf(
$conn,
'r.almanacServicePHID IN (%Ls)',
$this->almanacServicePHIDs);
}
return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
private function getNormalizedURIs() {
$normalized_uris = array();
// Since we don't know which type of repository this URI is in the general
// case, just generate all the normalizations. We could refine this in some
// cases: if the query specifies VCS types, or the URI is a git-style URI
// or an `svn+ssh` URI, we could deduce how to normalize it. However, this
// would be more complicated and it's not clear if it matters in practice.
$types = PhabricatorRepositoryURINormalizer::getAllURITypes();
foreach ($this->uris as $uri) {
foreach ($types as $type) {
$normalized_uri = new PhabricatorRepositoryURINormalizer($type, $uri);
$normalized_uris[] = $normalized_uri->getNormalizedURI();
}
}
return array_unique($normalized_uris);
}
}
diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php
index cc7b00e81..1e4867721 100644
--- a/src/applications/repository/storage/PhabricatorRepository.php
+++ b/src/applications/repository/storage/PhabricatorRepository.php
@@ -1,2856 +1,2850 @@
<?php
/**
* @task uri Repository URI Management
* @task autoclose Autoclose
* @task sync Cluster Synchronization
*/
final class PhabricatorRepository extends PhabricatorRepositoryDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorFlaggableInterface,
PhabricatorMarkupInterface,
PhabricatorDestructibleInterface,
PhabricatorDestructibleCodexInterface,
PhabricatorProjectInterface,
PhabricatorSpacesInterface,
PhabricatorConduitResultInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface {
/**
* Shortest hash we'll recognize in raw "a829f32" form.
*/
const MINIMUM_UNQUALIFIED_HASH = 7;
/**
* Shortest hash we'll recognize in qualified "rXab7ef2f8" form.
*/
const MINIMUM_QUALIFIED_HASH = 5;
/**
* Minimum number of commits to an empty repository to trigger "import" mode.
*/
const IMPORT_THRESHOLD = 7;
const TABLE_PATH = 'repository_path';
const TABLE_PATHCHANGE = 'repository_pathchange';
const TABLE_FILESYSTEM = 'repository_filesystem';
const TABLE_SUMMARY = 'repository_summary';
const TABLE_LINTMESSAGE = 'repository_lintmessage';
const TABLE_PARENTS = 'repository_parents';
const TABLE_COVERAGE = 'repository_coverage';
const BECAUSE_REPOSITORY_IMPORTING = 'auto/importing';
const BECAUSE_AUTOCLOSE_DISABLED = 'auto/disabled';
const BECAUSE_NOT_ON_AUTOCLOSE_BRANCH = 'auto/nobranch';
const BECAUSE_BRANCH_UNTRACKED = 'auto/notrack';
const BECAUSE_BRANCH_NOT_AUTOCLOSE = 'auto/noclose';
const BECAUSE_AUTOCLOSE_FORCED = 'auto/forced';
const STATUS_ACTIVE = 'active';
const STATUS_INACTIVE = 'inactive';
protected $name;
protected $callsign;
protected $repositorySlug;
protected $uuid;
protected $viewPolicy;
protected $editPolicy;
protected $pushPolicy;
protected $profileImagePHID;
protected $versionControlSystem;
protected $details = array();
protected $credentialPHID;
protected $almanacServicePHID;
protected $spacePHID;
protected $localPath;
private $commitCount = self::ATTACHABLE;
private $mostRecentCommit = self::ATTACHABLE;
private $projectPHIDs = self::ATTACHABLE;
private $uris = self::ATTACHABLE;
private $profileImageFile = self::ATTACHABLE;
public static function initializeNewRepository(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorDiffusionApplication'))
->executeOne();
$view_policy = $app->getPolicy(DiffusionDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(DiffusionDefaultEditCapability::CAPABILITY);
$push_policy = $app->getPolicy(DiffusionDefaultPushCapability::CAPABILITY);
$repository = id(new PhabricatorRepository())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setPushPolicy($push_policy)
->setSpacePHID($actor->getDefaultSpacePHID());
// Put the repository in "Importing" mode until we finish
// parsing it.
$repository->setDetail('importing', true);
// c4science customization
if($repository->getVersionControlSystem() == 'git') {
$repository->setFilesizeLimit(104900000);
}
return $repository;
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort255',
'callsign' => 'sort32?',
'repositorySlug' => 'sort64?',
'versionControlSystem' => 'text32',
'uuid' => 'text64?',
'pushPolicy' => 'policy',
'credentialPHID' => 'phid?',
'almanacServicePHID' => 'phid?',
'localPath' => 'text128?',
'profileImagePHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'callsign' => array(
'columns' => array('callsign'),
'unique' => true,
),
'key_name' => array(
'columns' => array('name(128)'),
),
'key_vcs' => array(
'columns' => array('versionControlSystem'),
),
'key_slug' => array(
'columns' => array('repositorySlug'),
'unique' => true,
),
'key_local' => array(
'columns' => array('localPath'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorRepositoryRepositoryPHIDType::TYPECONST);
}
public static function getStatusMap() {
return array(
self::STATUS_ACTIVE => array(
'name' => pht('Active'),
'isTracked' => 1,
),
self::STATUS_INACTIVE => array(
'name' => pht('Inactive'),
'isTracked' => 0,
),
);
}
public static function getStatusNameMap() {
return ipull(self::getStatusMap(), 'name');
}
public function getStatus() {
if ($this->isTracked()) {
return self::STATUS_ACTIVE;
} else {
return self::STATUS_INACTIVE;
}
}
public function toDictionary() {
return array(
'id' => $this->getID(),
'name' => $this->getName(),
'phid' => $this->getPHID(),
'callsign' => $this->getCallsign(),
'monogram' => $this->getMonogram(),
'vcs' => $this->getVersionControlSystem(),
'uri' => PhabricatorEnv::getProductionURI($this->getURI()),
'remoteURI' => (string)$this->getRemoteURI(),
'description' => $this->getDetail('description'),
'isActive' => $this->isTracked(),
'isHosted' => $this->isHosted(),
'isImporting' => $this->isImporting(),
'encoding' => $this->getDefaultTextEncoding(),
'staging' => array(
'supported' => $this->supportsStaging(),
'prefix' => 'phabricator',
'uri' => $this->getStagingURI(),
),
);
}
public function getDefaultTextEncoding() {
return $this->getDetail('encoding', 'UTF-8');
}
public function getMonogram() {
$callsign = $this->getCallsign();
if (strlen($callsign)) {
return "r{$callsign}";
}
$id = $this->getID();
return "R{$id}";
}
public function getDisplayName() {
$slug = $this->getRepositorySlug();
if (strlen($slug)) {
return $slug;
}
return $this->getMonogram();
}
public function getAllMonograms() {
$monograms = array();
$monograms[] = 'R'.$this->getID();
$callsign = $this->getCallsign();
if (strlen($callsign)) {
$monograms[] = 'r'.$callsign;
}
return $monograms;
}
public function setLocalPath($path) {
// Convert any extra slashes ("//") in the path to a single slash ("/").
$path = preg_replace('(//+)', '/', $path);
return parent::setLocalPath($path);
}
public function getDetail($key, $default = null) {
return idx($this->details, $key, $default);
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
}
public function attachCommitCount($count) {
$this->commitCount = $count;
return $this;
}
public function getCommitCount() {
return $this->assertAttached($this->commitCount);
}
public function attachMostRecentCommit(
PhabricatorRepositoryCommit $commit = null) {
$this->mostRecentCommit = $commit;
return $this;
}
public function getMostRecentCommit() {
return $this->assertAttached($this->mostRecentCommit);
}
public function getDiffusionBrowseURIForPath(
PhabricatorUser $user,
$path,
$line = null,
$branch = null) {
$drequest = DiffusionRequest::newFromDictionary(
array(
'user' => $user,
'repository' => $this,
'path' => $path,
'branch' => $branch,
));
return $drequest->generateURI(
array(
'action' => 'browse',
'line' => $line,
));
}
public function getSubversionBaseURI($commit = null) {
$subpath = $this->getDetail('svn-subpath');
if (!strlen($subpath)) {
$subpath = null;
}
return $this->getSubversionPathURI($subpath, $commit);
}
public function getSubversionPathURI($path = null, $commit = null) {
$vcs = $this->getVersionControlSystem();
if ($vcs != PhabricatorRepositoryType::REPOSITORY_TYPE_SVN) {
throw new Exception(pht('Not a subversion repository!'));
}
if ($this->isHosted()) {
$uri = 'file://'.$this->getLocalPath();
} else {
$uri = $this->getDetail('remote-uri');
}
$uri = rtrim($uri, '/');
if (strlen($path)) {
$path = rawurlencode($path);
$path = str_replace('%2F', '/', $path);
$uri = $uri.'/'.ltrim($path, '/');
}
if ($path !== null || $commit !== null) {
$uri .= '@';
}
if ($commit !== null) {
$uri .= $commit;
}
return $uri;
}
public function attachProjectPHIDs(array $project_phids) {
$this->projectPHIDs = $project_phids;
return $this;
}
public function getProjectPHIDs() {
return $this->assertAttached($this->projectPHIDs);
}
/**
* Get the name of the directory this repository should clone or checkout
* into. For example, if the repository name is "Example Repository", a
* reasonable name might be "example-repository". This is used to help users
* get reasonable results when cloning repositories, since they generally do
* not want to clone into directories called "X/" or "Example Repository/".
*
* @return string
*/
public function getCloneName() {
$name = $this->getRepositorySlug();
// Make some reasonable effort to produce reasonable default directory
// names from repository names.
if (!strlen($name)) {
$name = $this->getName();
$name = phutil_utf8_strtolower($name);
$name = preg_replace('@[ -/:->]+@', '-', $name);
$name = trim($name, '-');
if (!strlen($name)) {
$name = $this->getCallsign();
}
}
return $name;
}
public static function isValidRepositorySlug($slug) {
try {
self::assertValidRepositorySlug($slug);
return true;
} catch (Exception $ex) {
return false;
}
}
public static function assertValidRepositorySlug($slug) {
if (!strlen($slug)) {
throw new Exception(
pht(
'The empty string is not a valid repository short name. '.
'Repository short names must be at least one character long.'));
}
if (strlen($slug) > 64) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names must not be longer than 64 characters.',
$slug));
}
if (preg_match('/[^a-zA-Z0-9._-]/', $slug)) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names may only contain letters, numbers, periods, hyphens '.
'and underscores.',
$slug));
}
if (!preg_match('/^[a-zA-Z0-9]/', $slug)) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names must begin with a letter or number.',
$slug));
}
if (!preg_match('/[a-zA-Z0-9]\z/', $slug)) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names must end with a letter or number.',
$slug));
}
if (preg_match('/__|--|\\.\\./', $slug)) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names must not contain multiple consecutive underscores, '.
'hyphens, or periods.',
$slug));
}
if (preg_match('/^[A-Z]+\z/', $slug)) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names may not contain only uppercase letters.',
$slug));
}
if (preg_match('/^\d+\z/', $slug)) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names may not contain only numbers.',
$slug));
}
if (preg_match('/\\.git/', $slug)) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names must not end in ".git". This suffix will be added '.
'automatically in appropriate contexts.',
$slug));
}
}
public static function assertValidCallsign($callsign) {
if (!strlen($callsign)) {
throw new Exception(
pht(
'A repository callsign must be at least one character long.'));
}
if (strlen($callsign) > 32) {
throw new Exception(
pht(
'The callsign "%s" is not a valid repository callsign. Callsigns '.
'must be no more than 32 bytes long.',
$callsign));
}
if (!preg_match('/^[A-Z]+\z/', $callsign)) {
throw new Exception(
pht(
'The callsign "%s" is not a valid repository callsign. Callsigns '.
'may only contain UPPERCASE letters.',
$callsign));
}
}
public function getProfileImageURI() {
return $this->getProfileImageFile()->getBestURI();
}
public function attachProfileImageFile(PhabricatorFile $file) {
$this->profileImageFile = $file;
return $this;
}
public function getProfileImageFile() {
return $this->assertAttached($this->profileImageFile);
}
/* -( Remote Command Execution )------------------------------------------- */
public function execRemoteCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newRemoteCommandFuture($args)->resolve();
}
public function execxRemoteCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newRemoteCommandFuture($args)->resolvex();
}
public function getRemoteCommandFuture($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newRemoteCommandFuture($args);
}
public function passthruRemoteCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newRemoteCommandPassthru($args)->execute();
}
private function newRemoteCommandFuture(array $argv) {
return $this->newRemoteCommandEngine($argv)
->newFuture();
}
private function newRemoteCommandPassthru(array $argv) {
return $this->newRemoteCommandEngine($argv)
->setPassthru(true)
->newFuture();
}
private function newRemoteCommandEngine(array $argv) {
return DiffusionCommandEngine::newCommandEngine($this)
->setArgv($argv)
->setCredentialPHID($this->getCredentialPHID())
->setURI($this->getRemoteURIObject());
}
/* -( Local Command Execution )-------------------------------------------- */
public function execLocalCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newLocalCommandFuture($args)->resolve();
}
public function execxLocalCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newLocalCommandFuture($args)->resolvex();
}
public function getLocalCommandFuture($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newLocalCommandFuture($args);
}
public function passthruLocalCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newLocalCommandPassthru($args)->execute();
}
private function newLocalCommandFuture(array $argv) {
$this->assertLocalExists();
$future = DiffusionCommandEngine::newCommandEngine($this)
->setArgv($argv)
->newFuture();
if ($this->usesLocalWorkingCopy()) {
$future->setCWD($this->getLocalPath());
}
return $future;
}
private function newLocalCommandPassthru(array $argv) {
$this->assertLocalExists();
$future = DiffusionCommandEngine::newCommandEngine($this)
->setArgv($argv)
->setPassthru(true)
->newFuture();
if ($this->usesLocalWorkingCopy()) {
$future->setCWD($this->getLocalPath());
}
return $future;
}
public function getURI() {
$short_name = $this->getRepositorySlug();
if (strlen($short_name)) {
return "/source/{$short_name}/";
}
$callsign = $this->getCallsign();
if (strlen($callsign)) {
return "/diffusion/{$callsign}/";
}
$id = $this->getID();
return "/diffusion/{$id}/";
}
public function getPathURI($path) {
return $this->getURI().ltrim($path, '/');
}
public function getCommitURI($identifier) {
$callsign = $this->getCallsign();
if (strlen($callsign)) {
return "/r{$callsign}{$identifier}";
}
$id = $this->getID();
return "/R{$id}:{$identifier}";
}
public static function parseRepositoryServicePath($request_path, $vcs) {
$is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
$patterns = array(
'(^'.
'(?P<base>/?(?:diffusion|source)/(?P<identifier>[^/]+))'.
'(?P<path>.*)'.
'\z)',
);
$identifier = null;
foreach ($patterns as $pattern) {
$matches = null;
if (!preg_match($pattern, $request_path, $matches)) {
continue;
}
$identifier = $matches['identifier'];
if ($is_git) {
$identifier = preg_replace('/\\.git\z/', '', $identifier);
}
$base = $matches['base'];
$path = $matches['path'];
break;
}
if ($identifier === null) {
return null;
}
return array(
'identifier' => $identifier,
'base' => $base,
'path' => $path,
);
}
public function getCanonicalPath($request_path) {
$standard_pattern =
'(^'.
'(?P<prefix>/(?:diffusion|source)/)'.
'(?P<identifier>[^/]+)'.
'(?P<suffix>(?:/.*)?)'.
'\z)';
$matches = null;
if (preg_match($standard_pattern, $request_path, $matches)) {
$suffix = $matches['suffix'];
return $this->getPathURI($suffix);
}
$commit_pattern =
'(^'.
'(?P<prefix>/)'.
'(?P<monogram>'.
'(?:'.
'r(?P<repositoryCallsign>[A-Z]+)'.
'|'.
'R(?P<repositoryID>[1-9]\d*):'.
')'.
'(?P<commit>[a-f0-9]+)'.
')'.
'\z)';
$matches = null;
if (preg_match($commit_pattern, $request_path, $matches)) {
$commit = $matches['commit'];
return $this->getCommitURI($commit);
}
return null;
}
public function generateURI(array $params) {
$req_branch = false;
$req_commit = false;
$action = idx($params, 'action');
switch ($action) {
case 'history':
case 'graph':
case 'clone':
case 'jobs': // c4science custo
case 'blame':
case 'browse':
case 'document':
case 'change':
case 'lastmodified':
case 'tags':
case 'branches':
case 'lint':
case 'pathtree':
case 'refs':
case 'compare':
break;
case 'branch':
// NOTE: This does not actually require a branch, and won't have one
// in Subversion. Possibly this should be more clear.
break;
case 'commit':
case 'rendering-ref':
$req_commit = true;
break;
default:
throw new Exception(
pht(
'Action "%s" is not a valid repository URI action.',
$action));
}
$path = idx($params, 'path');
$branch = idx($params, 'branch');
$commit = idx($params, 'commit');
$line = idx($params, 'line');
$head = idx($params, 'head');
$against = idx($params, 'against');
if ($req_commit && !strlen($commit)) {
throw new Exception(
pht(
'Diffusion URI action "%s" requires commit!',
$action));
}
if ($req_branch && !strlen($branch)) {
throw new Exception(
pht(
'Diffusion URI action "%s" requires branch!',
$action));
}
if ($action === 'commit') {
return $this->getCommitURI($commit);
}
if (strlen($path)) {
$path = ltrim($path, '/');
$path = str_replace(array(';', '$'), array(';;', '$$'), $path);
$path = phutil_escape_uri($path);
}
$raw_branch = $branch;
if (strlen($branch)) {
$branch = phutil_escape_uri_path_component($branch);
$path = "{$branch}/{$path}";
}
$raw_commit = $commit;
if (strlen($commit)) {
$commit = str_replace('$', '$$', $commit);
$commit = ';'.phutil_escape_uri($commit);
}
if (strlen($line)) {
$line = '$'.phutil_escape_uri($line);
}
$query = array();
switch ($action) {
case 'change':
case 'history':
case 'graph':
case 'jobs': // c4science custo
case 'blame':
case 'browse':
case 'document':
case 'lastmodified':
case 'tags':
case 'branches':
case 'lint':
case 'pathtree':
case 'refs':
$uri = $this->getPathURI("/{$action}/{$path}{$commit}{$line}");
break;
case 'compare':
$uri = $this->getPathURI("/{$action}/");
if (strlen($head)) {
$query['head'] = $head;
} else if (strlen($raw_commit)) {
$query['commit'] = $raw_commit;
} else if (strlen($raw_branch)) {
$query['head'] = $raw_branch;
}
if (strlen($against)) {
$query['against'] = $against;
}
break;
case 'branch':
if (strlen($path)) {
$uri = $this->getPathURI("/repository/{$path}");
} else {
$uri = $this->getPathURI('/');
}
break;
case 'external':
$commit = ltrim($commit, ';');
$uri = "/diffusion/external/{$commit}/";
break;
case 'rendering-ref':
// This isn't a real URI per se, it's passed as a query parameter to
// the ajax changeset stuff but then we parse it back out as though
// it came from a URI.
$uri = rawurldecode("{$path}{$commit}");
break;
case 'clone':
$uri = $this->getPathURI("/{$action}/");
break;
}
if ($action == 'rendering-ref') {
return $uri;
}
- $uri = new PhutilURI($uri);
-
if (isset($params['lint'])) {
$params['params'] = idx($params, 'params', array()) + array(
'lint' => $params['lint'],
);
}
$query = idx($params, 'params', array()) + $query;
- if ($query) {
- $uri->setQueryParams($query);
- }
-
- return $uri;
+ return new PhutilURI($uri, $query);
}
public function updateURIIndex() {
$indexes = array();
$uris = $this->getURIs();
foreach ($uris as $uri) {
if ($uri->getIsDisabled()) {
continue;
}
$indexes[] = $uri->getNormalizedURI();
}
PhabricatorRepositoryURIIndex::updateRepositoryURIs(
$this->getPHID(),
$indexes);
return $this;
}
public function isTracked() {
$status = $this->getDetail('tracking-enabled');
$map = self::getStatusMap();
$spec = idx($map, $status);
if (!$spec) {
if ($status) {
$status = self::STATUS_ACTIVE;
} else {
$status = self::STATUS_INACTIVE;
}
$spec = idx($map, $status);
}
return (bool)idx($spec, 'isTracked', false);
}
public function getDefaultBranch() {
$default = $this->getDetail('default-branch');
if (strlen($default)) {
return $default;
}
$default_branches = array(
PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'master',
PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => 'default',
);
return idx($default_branches, $this->getVersionControlSystem());
}
public function getDefaultArcanistBranch() {
return coalesce($this->getDefaultBranch(), 'svn');
}
private function isBranchInFilter($branch, $filter_key) {
$vcs = $this->getVersionControlSystem();
$is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
$use_filter = ($is_git);
if (!$use_filter) {
// If this VCS doesn't use filters, pass everything through.
return true;
}
$filter = $this->getDetail($filter_key, array());
// If there's no filter set, let everything through.
if (!$filter) {
return true;
}
// If this branch isn't literally named `regexp(...)`, and it's in the
// filter list, let it through.
if (isset($filter[$branch])) {
if (self::extractBranchRegexp($branch) === null) {
return true;
}
}
// If the branch matches a regexp, let it through.
foreach ($filter as $pattern => $ignored) {
$regexp = self::extractBranchRegexp($pattern);
if ($regexp !== null) {
if (preg_match($regexp, $branch)) {
return true;
}
}
}
// Nothing matched, so filter this branch out.
return false;
}
public static function extractBranchRegexp($pattern) {
$matches = null;
if (preg_match('/^regexp\\((.*)\\)\z/', $pattern, $matches)) {
return $matches[1];
}
return null;
}
public function shouldTrackRef(DiffusionRepositoryRef $ref) {
// At least for now, don't track the staging area tags.
if ($ref->isTag()) {
if (preg_match('(^phabricator/)', $ref->getShortName())) {
return false;
}
}
if (!$ref->isBranch()) {
return true;
}
return $this->shouldTrackBranch($ref->getShortName());
}
public function shouldTrackBranch($branch) {
return $this->isBranchInFilter($branch, 'branch-filter');
}
public function formatCommitName($commit_identifier, $local = false) {
$vcs = $this->getVersionControlSystem();
$type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
$type_hg = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL;
$is_git = ($vcs == $type_git);
$is_hg = ($vcs == $type_hg);
if ($is_git || $is_hg) {
$name = substr($commit_identifier, 0, 12);
$need_scope = false;
} else {
$name = $commit_identifier;
$need_scope = true;
}
if (!$local) {
$need_scope = true;
}
if ($need_scope) {
$callsign = $this->getCallsign();
if ($callsign) {
$scope = "r{$callsign}";
} else {
$id = $this->getID();
$scope = "R{$id}:";
}
$name = $scope.$name;
}
return $name;
}
public function isImporting() {
return (bool)$this->getDetail('importing', false);
}
public function isNewlyInitialized() {
return (bool)$this->getDetail('newly-initialized', false);
}
public function loadImportProgress() {
$progress = queryfx_all(
$this->establishConnection('r'),
'SELECT importStatus, count(*) N FROM %T WHERE repositoryID = %d
GROUP BY importStatus',
id(new PhabricatorRepositoryCommit())->getTableName(),
$this->getID());
$done = 0;
$total = 0;
foreach ($progress as $row) {
$total += $row['N'] * 4;
$status = $row['importStatus'];
if ($status & PhabricatorRepositoryCommit::IMPORTED_MESSAGE) {
$done += $row['N'];
}
if ($status & PhabricatorRepositoryCommit::IMPORTED_CHANGE) {
$done += $row['N'];
}
if ($status & PhabricatorRepositoryCommit::IMPORTED_OWNERS) {
$done += $row['N'];
}
if ($status & PhabricatorRepositoryCommit::IMPORTED_HERALD) {
$done += $row['N'];
}
}
if ($total) {
$ratio = ($done / $total);
} else {
$ratio = 0;
}
// Cap this at "99.99%", because it's confusing to users when the actual
// fraction is "99.996%" and it rounds up to "100.00%".
if ($ratio > 0.9999) {
$ratio = 0.9999;
}
return $ratio;
}
/**
* Should this repository publish feed, notifications, audits, and email?
*
* We do not publish information about repositories during initial import,
* or if the repository has been set not to publish.
*/
public function shouldPublish() {
if ($this->isImporting()) {
return false;
}
if ($this->getDetail('herald-disabled')) {
return false;
}
return true;
}
/* -( Autoclose )---------------------------------------------------------- */
public function shouldAutocloseRef(DiffusionRepositoryRef $ref) {
if (!$ref->isBranch()) {
return false;
}
return $this->shouldAutocloseBranch($ref->getShortName());
}
/**
* Determine if autoclose is active for a branch.
*
* For more details about why, use @{method:shouldSkipAutocloseBranch}.
*
* @param string Branch name to check.
* @return bool True if autoclose is active for the branch.
* @task autoclose
*/
public function shouldAutocloseBranch($branch) {
return ($this->shouldSkipAutocloseBranch($branch) === null);
}
/**
* Determine if autoclose is active for a commit.
*
* For more details about why, use @{method:shouldSkipAutocloseCommit}.
*
* @param PhabricatorRepositoryCommit Commit to check.
* @return bool True if autoclose is active for the commit.
* @task autoclose
*/
public function shouldAutocloseCommit(PhabricatorRepositoryCommit $commit) {
return ($this->shouldSkipAutocloseCommit($commit) === null);
}
/**
* Determine why autoclose should be skipped for a branch.
*
* This method gives a detailed reason why autoclose will be skipped. To
* perform a simple test, use @{method:shouldAutocloseBranch}.
*
* @param string Branch name to check.
* @return const|null Constant identifying reason to skip this branch, or null
* if autoclose is active.
* @task autoclose
*/
public function shouldSkipAutocloseBranch($branch) {
$all_reason = $this->shouldSkipAllAutoclose();
if ($all_reason) {
return $all_reason;
}
if (!$this->shouldTrackBranch($branch)) {
return self::BECAUSE_BRANCH_UNTRACKED;
}
if (!$this->isBranchInFilter($branch, 'close-commits-filter')) {
return self::BECAUSE_BRANCH_NOT_AUTOCLOSE;
}
return null;
}
/**
* Determine why autoclose should be skipped for a commit.
*
* This method gives a detailed reason why autoclose will be skipped. To
* perform a simple test, use @{method:shouldAutocloseCommit}.
*
* @param PhabricatorRepositoryCommit Commit to check.
* @return const|null Constant identifying reason to skip this commit, or null
* if autoclose is active.
* @task autoclose
*/
public function shouldSkipAutocloseCommit(
PhabricatorRepositoryCommit $commit) {
$all_reason = $this->shouldSkipAllAutoclose();
if ($all_reason) {
return $all_reason;
}
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return null;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
break;
default:
throw new Exception(pht('Unrecognized version control system.'));
}
$closeable_flag = PhabricatorRepositoryCommit::IMPORTED_CLOSEABLE;
if (!$commit->isPartiallyImported($closeable_flag)) {
return self::BECAUSE_NOT_ON_AUTOCLOSE_BRANCH;
}
return null;
}
/**
* Determine why all autoclose operations should be skipped for this
* repository.
*
* @return const|null Constant identifying reason to skip all autoclose
* operations, or null if autoclose operations are not blocked at the
* repository level.
* @task autoclose
*/
private function shouldSkipAllAutoclose() {
if ($this->isImporting()) {
return self::BECAUSE_REPOSITORY_IMPORTING;
}
if ($this->getDetail('disable-autoclose', false)) {
return self::BECAUSE_AUTOCLOSE_DISABLED;
}
return null;
}
public function getAutocloseOnlyRules() {
return array_keys($this->getDetail('close-commits-filter', array()));
}
public function setAutocloseOnlyRules(array $rules) {
$rules = array_fill_keys($rules, true);
$this->setDetail('close-commits-filter', $rules);
return $this;
}
public function getTrackOnlyRules() {
return array_keys($this->getDetail('branch-filter', array()));
}
public function setTrackOnlyRules(array $rules) {
$rules = array_fill_keys($rules, true);
$this->setDetail('branch-filter', $rules);
return $this;
}
/* -( Repository URI Management )------------------------------------------ */
/**
* Get the remote URI for this repository.
*
* @return string
* @task uri
*/
public function getRemoteURI() {
return (string)$this->getRemoteURIObject();
}
/**
* Get the remote URI for this repository, including credentials if they're
* used by this repository.
*
* @return PhutilOpaqueEnvelope URI, possibly including credentials.
* @task uri
*/
public function getRemoteURIEnvelope() {
$uri = $this->getRemoteURIObject();
$remote_protocol = $this->getRemoteProtocol();
if ($remote_protocol == 'http' || $remote_protocol == 'https') {
// For SVN, we use `--username` and `--password` flags separately, so
// don't add any credentials here.
if (!$this->isSVN()) {
$credential_phid = $this->getCredentialPHID();
if ($credential_phid) {
$key = PassphrasePasswordKey::loadFromPHID(
$credential_phid,
PhabricatorUser::getOmnipotentUser());
$uri->setUser($key->getUsernameEnvelope()->openEnvelope());
$uri->setPass($key->getPasswordEnvelope()->openEnvelope());
}
}
}
return new PhutilOpaqueEnvelope((string)$uri);
}
/**
* Get the clone (or checkout) URI for this repository, without authentication
* information.
*
* @return string Repository URI.
* @task uri
*/
public function getPublicCloneURI() {
return (string)$this->getCloneURIObject();
}
/**
* Get the protocol for the repository's remote.
*
* @return string Protocol, like "ssh" or "git".
* @task uri
*/
public function getRemoteProtocol() {
$uri = $this->getRemoteURIObject();
return $uri->getProtocol();
}
/**
* Get a parsed object representation of the repository's remote URI..
*
* @return wild A @{class@libphutil:PhutilURI}.
* @task uri
*/
public function getRemoteURIObject() {
$raw_uri = $this->getDetail('remote-uri');
if (!strlen($raw_uri)) {
return new PhutilURI('');
}
if (!strncmp($raw_uri, '/', 1)) {
return new PhutilURI('file://'.$raw_uri);
}
return new PhutilURI($raw_uri);
}
/**
* Get the "best" clone/checkout URI for this repository, on any protocol.
*/
public function getCloneURIObject() {
if (!$this->isHosted()) {
if ($this->isSVN()) {
// Make sure we pick up the "Import Only" path for Subversion, so
// the user clones the repository starting at the correct path, not
// from the root.
$base_uri = $this->getSubversionBaseURI();
$base_uri = new PhutilURI($base_uri);
$path = $base_uri->getPath();
if (!$path) {
$path = '/';
}
// If the trailing "@" is not required to escape the URI, strip it for
// readability.
if (!preg_match('/@.*@/', $path)) {
$path = rtrim($path, '@');
}
$base_uri->setPath($path);
return $base_uri;
} else {
return $this->getRemoteURIObject();
}
}
// TODO: This should be cleaned up to deal with all the new URI handling.
$another_copy = id(new PhabricatorRepositoryQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($this->getPHID()))
->needURIs(true)
->executeOne();
$clone_uris = $another_copy->getCloneURIs();
if (!$clone_uris) {
return null;
}
return head($clone_uris)->getEffectiveURI();
}
private function getRawHTTPCloneURIObject() {
$uri = PhabricatorEnv::getProductionURI($this->getURI());
$uri = new PhutilURI($uri);
if ($this->isGit()) {
$uri->setPath($uri->getPath().$this->getCloneName().'.git');
} else if ($this->isHg()) {
$uri->setPath($uri->getPath().$this->getCloneName().'/');
}
return $uri;
}
/**
* Determine if we should connect to the remote using SSH flags and
* credentials.
*
* @return bool True to use the SSH protocol.
* @task uri
*/
private function shouldUseSSH() {
if ($this->isHosted()) {
return false;
}
$protocol = $this->getRemoteProtocol();
if ($this->isSSHProtocol($protocol)) {
return true;
}
return false;
}
/**
* Determine if we should connect to the remote using HTTP flags and
* credentials.
*
* @return bool True to use the HTTP protocol.
* @task uri
*/
private function shouldUseHTTP() {
if ($this->isHosted()) {
return false;
}
$protocol = $this->getRemoteProtocol();
return ($protocol == 'http' || $protocol == 'https');
}
/**
* Determine if we should connect to the remote using SVN flags and
* credentials.
*
* @return bool True to use the SVN protocol.
* @task uri
*/
private function shouldUseSVNProtocol() {
if ($this->isHosted()) {
return false;
}
$protocol = $this->getRemoteProtocol();
return ($protocol == 'svn');
}
/**
* Determine if a protocol is SSH or SSH-like.
*
* @param string A protocol string, like "http" or "ssh".
* @return bool True if the protocol is SSH-like.
* @task uri
*/
private function isSSHProtocol($protocol) {
return ($protocol == 'ssh' || $protocol == 'svn+ssh');
}
public function delete() {
$this->openTransaction();
$paths = id(new PhabricatorOwnersPath())
->loadAllWhere('repositoryPHID = %s', $this->getPHID());
foreach ($paths as $path) {
$path->delete();
}
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE repositoryPHID = %s',
id(new PhabricatorRepositorySymbol())->getTableName(),
$this->getPHID());
$commits = id(new PhabricatorRepositoryCommit())
->loadAllWhere('repositoryID = %d', $this->getID());
foreach ($commits as $commit) {
// note PhabricatorRepositoryAuditRequests and
// PhabricatorRepositoryCommitData are deleted here too.
$commit->delete();
}
$uris = id(new PhabricatorRepositoryURI())
->loadAllWhere('repositoryPHID = %s', $this->getPHID());
foreach ($uris as $uri) {
$uri->delete();
}
$ref_cursors = id(new PhabricatorRepositoryRefCursor())
->loadAllWhere('repositoryPHID = %s', $this->getPHID());
foreach ($ref_cursors as $cursor) {
$cursor->delete();
}
$conn_w = $this->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d',
self::TABLE_FILESYSTEM,
$this->getID());
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d',
self::TABLE_PATHCHANGE,
$this->getID());
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d',
self::TABLE_SUMMARY,
$this->getID());
$result = parent::delete();
$this->saveTransaction();
return $result;
}
public function isGit() {
$vcs = $this->getVersionControlSystem();
return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
}
public function isSVN() {
$vcs = $this->getVersionControlSystem();
return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_SVN);
}
public function isHg() {
$vcs = $this->getVersionControlSystem();
return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL);
}
public function isHosted() {
return (bool)$this->getDetail('hosting-enabled', false);
}
public function setHosted($enabled) {
return $this->setDetail('hosting-enabled', $enabled);
}
public function canServeProtocol(
$protocol,
$write,
$is_intracluster = false) {
// See T13192. If a repository is inactive, don't serve it to users. We
// still synchronize it within the cluster and serve it to other repository
// nodes.
if (!$is_intracluster) {
if (!$this->isTracked()) {
return false;
}
}
$clone_uris = $this->getCloneURIs();
foreach ($clone_uris as $uri) {
if ($uri->getBuiltinProtocol() !== $protocol) {
continue;
}
$io_type = $uri->getEffectiveIoType();
if ($io_type == PhabricatorRepositoryURI::IO_READWRITE) {
return true;
}
if (!$write) {
if ($io_type == PhabricatorRepositoryURI::IO_READ) {
return true;
}
}
}
return false;
}
public function hasLocalWorkingCopy() {
try {
self::assertLocalExists();
return true;
} catch (Exception $ex) {
return false;
}
}
/**
* Raise more useful errors when there are basic filesystem problems.
*/
private function assertLocalExists() {
if (!$this->usesLocalWorkingCopy()) {
return;
}
$local = $this->getLocalPath();
Filesystem::assertExists($local);
Filesystem::assertIsDirectory($local);
Filesystem::assertReadable($local);
}
/**
* Determine if the working copy is bare or not. In Git, this corresponds
* to `--bare`. In Mercurial, `--noupdate`.
*/
public function isWorkingCopyBare() {
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return false;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$local = $this->getLocalPath();
if (Filesystem::pathExists($local.'/.git')) {
return false;
} else {
return true;
}
}
}
public function usesLocalWorkingCopy() {
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
return $this->isHosted();
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return true;
}
}
public function getHookDirectories() {
$directories = array();
if (!$this->isHosted()) {
return $directories;
}
$root = $this->getLocalPath();
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
if ($this->isWorkingCopyBare()) {
$directories[] = $root.'/hooks/pre-receive-phabricator.d/';
} else {
$directories[] = $root.'/.git/hooks/pre-receive-phabricator.d/';
}
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$directories[] = $root.'/hooks/pre-commit-phabricator.d/';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
// NOTE: We don't support custom Mercurial hooks for now because they're
// messy and we can't easily just drop a `hooks.d/` directory next to
// the hooks.
break;
}
return $directories;
}
public function canDestroyWorkingCopy() {
if ($this->isHosted()) {
// Never destroy hosted working copies.
return false;
}
$default_path = PhabricatorEnv::getEnvConfig(
'repository.default-local-path');
return Filesystem::isDescendant($this->getLocalPath(), $default_path);
}
public function canUsePathTree() {
return !$this->isSVN();
}
public function canUseGitLFS() {
if (!$this->isGit()) {
return false;
}
if (!$this->isHosted()) {
return false;
}
if (!PhabricatorEnv::getEnvConfig('diffusion.allow-git-lfs')) {
return false;
}
return true;
}
public function getGitLFSURI($path = null) {
if (!$this->canUseGitLFS()) {
throw new Exception(
pht(
'This repository does not support Git LFS, so Git LFS URIs can '.
'not be generated for it.'));
}
$uri = $this->getRawHTTPCloneURIObject();
$uri = (string)$uri;
$uri = $uri.'/'.$path;
return $uri;
}
public function canMirror() {
if ($this->isGit() || $this->isHg()) {
return true;
}
return false;
}
public function canAllowDangerousChanges() {
if (!$this->isHosted()) {
return false;
}
// In Git and Mercurial, ref deletions and rewrites are dangerous.
// In Subversion, editing revprops is dangerous.
return true;
}
public function shouldAllowDangerousChanges() {
return (bool)$this->getDetail('allow-dangerous-changes');
}
public function canAllowEnormousChanges() {
if (!$this->isHosted()) {
return false;
}
return true;
}
public function shouldAllowEnormousChanges() {
return (bool)$this->getDetail('allow-enormous-changes');
}
public function writeStatusMessage(
$status_type,
$status_code,
array $parameters = array()) {
$table = new PhabricatorRepositoryStatusMessage();
$conn_w = $table->establishConnection('w');
$table_name = $table->getTableName();
if ($status_code === null) {
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d AND statusType = %s',
$table_name,
$this->getID(),
$status_type);
} else {
// If the existing message has the same code (e.g., we just hit an
// error and also previously hit an error) we increment the message
// count. This allows us to determine how many times in a row we've
// run into an error.
// NOTE: The assignments in "ON DUPLICATE KEY UPDATE" are evaluated
// in order, so the "messageCount" assignment must occur before the
// "statusCode" assignment. See T11705.
queryfx(
$conn_w,
'INSERT INTO %T
(repositoryID, statusType, statusCode, parameters, epoch,
messageCount)
VALUES (%d, %s, %s, %s, %d, %d)
ON DUPLICATE KEY UPDATE
messageCount =
IF(
statusCode = VALUES(statusCode),
messageCount + VALUES(messageCount),
VALUES(messageCount)),
statusCode = VALUES(statusCode),
parameters = VALUES(parameters),
epoch = VALUES(epoch)',
$table_name,
$this->getID(),
$status_type,
$status_code,
json_encode($parameters),
time(),
1);
}
return $this;
}
public static function assertValidRemoteURI($uri) {
if (trim($uri) != $uri) {
throw new Exception(
pht('The remote URI has leading or trailing whitespace.'));
}
$uri_object = new PhutilURI($uri);
$protocol = $uri_object->getProtocol();
// Catch confusion between Git/SCP-style URIs and normal URIs. See T3619
// for discussion. This is usually a user adding "ssh://" to an implicit
// SSH Git URI.
if ($protocol == 'ssh') {
if (preg_match('(^[^:@]+://[^/:]+:[^\d])', $uri)) {
throw new Exception(
pht(
"The remote URI is not formatted correctly. Remote URIs ".
"with an explicit protocol should be in the form ".
"'%s', not '%s'. The '%s' syntax is only valid in SCP-style URIs.",
'proto://domain/path',
'proto://domain:/path',
':/path'));
}
}
switch ($protocol) {
case 'ssh':
case 'http':
case 'https':
case 'git':
case 'svn':
case 'svn+ssh':
break;
default:
// NOTE: We're explicitly rejecting 'file://' because it can be
// used to clone from the working copy of another repository on disk
// that you don't normally have permission to access.
throw new Exception(
pht(
'The URI protocol is unrecognized. It should begin with '.
'"%s", "%s", "%s", "%s", "%s", "%s", or be in the form "%s".',
'ssh://',
'http://',
'https://',
'git://',
'svn://',
'svn+ssh://',
'git@domain.com:path'));
}
return true;
}
/**
* Load the pull frequency for this repository, based on the time since the
* last activity.
*
* We pull rarely used repositories less frequently. This finds the most
* recent commit which is older than the current time (which prevents us from
* spinning on repositories with a silly commit post-dated to some time in
* 2037). We adjust the pull frequency based on when the most recent commit
* occurred.
*
* @param int The minimum update interval to use, in seconds.
* @return int Repository update interval, in seconds.
*/
public function loadUpdateInterval($minimum = 15) {
// First, check if we've hit errors recently. If we have, wait one period
// for each consecutive error. Normally, this corresponds to a backoff of
// 15s, 30s, 45s, etc.
$message_table = new PhabricatorRepositoryStatusMessage();
$conn = $message_table->establishConnection('r');
$error_count = queryfx_one(
$conn,
'SELECT MAX(messageCount) error_count FROM %T
WHERE repositoryID = %d
AND statusType IN (%Ls)
AND statusCode IN (%Ls)',
$message_table->getTableName(),
$this->getID(),
array(
PhabricatorRepositoryStatusMessage::TYPE_INIT,
PhabricatorRepositoryStatusMessage::TYPE_FETCH,
),
array(
PhabricatorRepositoryStatusMessage::CODE_ERROR,
));
$error_count = (int)$error_count['error_count'];
if ($error_count > 0) {
return (int)($minimum * $error_count);
}
// If a repository is still importing, always pull it as frequently as
// possible. This prevents us from hanging for a long time at 99.9% when
// importing an inactive repository.
if ($this->isImporting()) {
return $minimum;
}
$window_start = (PhabricatorTime::getNow() + $minimum);
$table = id(new PhabricatorRepositoryCommit());
$last_commit = queryfx_one(
$table->establishConnection('r'),
'SELECT epoch FROM %T
WHERE repositoryID = %d AND epoch <= %d
ORDER BY epoch DESC LIMIT 1',
$table->getTableName(),
$this->getID(),
$window_start);
if ($last_commit) {
$time_since_commit = ($window_start - $last_commit['epoch']);
} else {
// If the repository has no commits, treat the creation date as
// though it were the date of the last commit. This makes empty
// repositories update quickly at first but slow down over time
// if they don't see any activity.
$time_since_commit = ($window_start - $this->getDateCreated());
}
$last_few_days = phutil_units('3 days in seconds');
if ($time_since_commit <= $last_few_days) {
// For repositories with activity in the recent past, we wait one
// extra second for every 10 minutes since the last commit. This
// shorter backoff is intended to handle weekends and other short
// breaks from development.
$smart_wait = ($time_since_commit / 600);
} else {
// For repositories without recent activity, we wait one extra second
// for every 4 minutes since the last commit. This longer backoff
// handles rarely used repositories, up to the maximum.
$smart_wait = ($time_since_commit / 240);
}
// We'll never wait more than 6 hours to pull a repository.
$longest_wait = phutil_units('6 hours in seconds');
$smart_wait = min($smart_wait, $longest_wait);
$smart_wait = max($minimum, $smart_wait);
return (int)$smart_wait;
}
/**
* Time limit for cloning or copying this repository.
*
* This limit is used to timeout operations like `git clone` or `git fetch`
* when doing intracluster synchronization, building working copies, etc.
*
* @return int Maximum number of seconds to spend copying this repository.
*/
public function getCopyTimeLimit() {
return $this->getDetail('limit.copy');
}
public function setCopyTimeLimit($limit) {
return $this->setDetail('limit.copy', $limit);
}
public function getDefaultCopyTimeLimit() {
return phutil_units('15 minutes in seconds');
}
public function getEffectiveCopyTimeLimit() {
$limit = $this->getCopyTimeLimit();
if ($limit) {
return $limit;
}
return $this->getDefaultCopyTimeLimit();
}
public function getFilesizeLimit() {
return $this->getDetail('limit.filesize');
}
public function setFilesizeLimit($limit) {
return $this->setDetail('limit.filesize', $limit);
}
public function getTouchLimit() {
return $this->getDetail('limit.touch');
}
public function setTouchLimit($limit) {
return $this->setDetail('limit.touch', $limit);
}
/**
* Retrieve the service URI for the device hosting this repository.
*
* See @{method:newConduitClient} for a general discussion of interacting
* with repository services. This method provides lower-level resolution of
* services, returning raw URIs.
*
* @param PhabricatorUser Viewing user.
* @param map<string, wild> Constraints on selectable services.
* @return string|null URI, or `null` for local repositories.
*/
public function getAlmanacServiceURI(
PhabricatorUser $viewer,
array $options) {
PhutilTypeSpec::checkMap(
$options,
array(
'neverProxy' => 'bool',
'protocols' => 'list<string>',
'writable' => 'optional bool',
));
$never_proxy = $options['neverProxy'];
$protocols = $options['protocols'];
$writable = idx($options, 'writable', false);
$cache_key = $this->getAlmanacServiceCacheKey();
if (!$cache_key) {
return null;
}
$cache = PhabricatorCaches::getMutableStructureCache();
$uris = $cache->getKey($cache_key, false);
// If we haven't built the cache yet, build it now.
if ($uris === false) {
$uris = $this->buildAlmanacServiceURIs();
$cache->setKey($cache_key, $uris);
}
if ($uris === null) {
return null;
}
$local_device = AlmanacKeys::getDeviceID();
if ($never_proxy && !$local_device) {
throw new Exception(
pht(
'Unable to handle proxied service request. This device is not '.
'registered, so it can not identify local services. Register '.
'this device before sending requests here.'));
}
$protocol_map = array_fuse($protocols);
$results = array();
foreach ($uris as $uri) {
// If we're never proxying this and it's locally satisfiable, return
// `null` to tell the caller to handle it locally. If we're allowed to
// proxy, we skip this check and may proxy the request to ourselves.
// (That proxied request will end up here with proxying forbidden,
// return `null`, and then the request will actually run.)
if ($local_device && $never_proxy) {
if ($uri['device'] == $local_device) {
return null;
}
}
if (isset($protocol_map[$uri['protocol']])) {
$results[] = $uri;
}
}
if (!$results) {
throw new Exception(
pht(
'The Almanac service for this repository is not bound to any '.
'interfaces which support the required protocols (%s).',
implode(', ', $protocols)));
}
if ($never_proxy) {
// See PHI1030. This error can arise from various device name/address
// mismatches which are hard to detect, so try to provide as much
// information as we can.
if ($writable) {
$request_type = pht('(This is a write request.)');
} else {
$request_type = pht('(This is a read request.)');
}
throw new Exception(
pht(
'This repository request (for repository "%s") has been '.
'incorrectly routed to a cluster host (with device name "%s", '.
'and hostname "%s") which can not serve the request.'.
"\n\n".
'The Almanac device address for the correct device may improperly '.
'point at this host, or the "device.id" configuration file on '.
'this host may be incorrect.'.
"\n\n".
'Requests routed within the cluster by Phabricator are always '.
'expected to be sent to a node which can serve the request. To '.
'prevent loops, this request will not be proxied again.'.
"\n\n".
"%s",
$this->getDisplayName(),
$local_device,
php_uname('n'),
$request_type));
}
if (count($results) > 1) {
if (!$this->supportsSynchronization()) {
throw new Exception(
pht(
'Repository "%s" is bound to multiple active repository hosts, '.
'but this repository does not support cluster synchronization. '.
'Declusterize this repository or move it to a service with only '.
'one host.',
$this->getDisplayName()));
}
}
// 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)));
}
}
if ($writable) {
$results = $this->sortWritableAlmanacServiceURIs($results);
} else {
shuffle($results);
}
$result = head($results);
return $result['uri'];
}
private function sortWritableAlmanacServiceURIs(array $results) {
// See T13109 for discussion of how this method routes requests.
// In the absence of other rules, we'll send traffic to devices randomly.
// We also want to select randomly among nodes which are equally good
// candidates to receive the write, and accomplish that by shuffling the
// list up front.
shuffle($results);
$order = array();
// If some device is currently holding the write lock, send all requests
// to that device. We're trying to queue writes on a single device so they
// do not need to wait for read synchronization after earlier writes
// complete.
$writer = PhabricatorRepositoryWorkingCopyVersion::loadWriter(
$this->getPHID());
if ($writer) {
$device_phid = $writer->getWriteProperty('devicePHID');
foreach ($results as $key => $result) {
if ($result['devicePHID'] === $device_phid) {
$order[] = $key;
}
}
}
// If no device is currently holding the write lock, try to send requests
// to a device which is already up to date and will not need to synchronize
// before it can accept the write.
$versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions(
$this->getPHID());
if ($versions) {
$max_version = (int)max(mpull($versions, 'getRepositoryVersion'));
$max_devices = array();
foreach ($versions as $version) {
if ($version->getRepositoryVersion() == $max_version) {
$max_devices[] = $version->getDevicePHID();
}
}
$max_devices = array_fuse($max_devices);
foreach ($results as $key => $result) {
if (isset($max_devices[$result['devicePHID']])) {
$order[] = $key;
}
}
}
// Reorder the results, putting any we've selected as preferred targets for
// the write at the head of the list.
$results = array_select_keys($results, $order) + $results;
return $results;
}
public function supportsSynchronization() {
// TODO: For now, this is only supported for Git.
if (!$this->isGit()) {
return false;
}
return true;
}
public function getAlmanacServiceCacheKey() {
$service_phid = $this->getAlmanacServicePHID();
if (!$service_phid) {
return null;
}
$repository_phid = $this->getPHID();
$parts = array(
"repo({$repository_phid})",
"serv({$service_phid})",
'v4',
);
return implode('.', $parts);
}
private function buildAlmanacServiceURIs() {
$service = $this->loadAlmanacService();
if (!$service) {
return null;
}
$bindings = $service->getActiveBindings();
if (!$bindings) {
throw new Exception(
pht(
'The Almanac service for this repository is not bound to any '.
'interfaces.'));
}
$uris = array();
foreach ($bindings as $binding) {
$iface = $binding->getInterface();
$uri = $this->getClusterRepositoryURIFromBinding($binding);
$protocol = $uri->getProtocol();
$device_name = $iface->getDevice()->getName();
$device_phid = $iface->getDevice()->getPHID();
$uris[] = array(
'protocol' => $protocol,
'uri' => (string)$uri,
'device' => $device_name,
'writable' => (bool)$binding->getAlmanacPropertyValue('writable'),
'devicePHID' => $device_phid,
);
}
return $uris;
}
/**
* Build a new Conduit client in order to make a service call to this
* repository.
*
* If the repository is hosted locally, this method may return `null`. The
* caller should use `ConduitCall` or other local logic to complete the
* request.
*
* By default, we will return a @{class:ConduitClient} for any repository with
* a service, even if that service is on the current device.
*
* We do this because this configuration does not make very much sense in a
* production context, but is very common in a test/development context
* (where the developer's machine is both the web host and the repository
* service). By proxying in development, we get more consistent behavior
* between development and production, and don't have a major untested
* codepath.
*
* The `$never_proxy` parameter can be used to prevent this local proxying.
* If the flag is passed:
*
* - The method will return `null` (implying a local service call)
* if the repository service is hosted on the current device.
* - The method will throw if it would need to return a client.
*
* This is used to prevent loops in Conduit: the first request will proxy,
* even in development, but the second request will be identified as a
* cluster request and forced not to proxy.
*
* For lower-level service resolution, see @{method:getAlmanacServiceURI}.
*
* @param PhabricatorUser Viewing user.
* @param bool `true` to throw if a client would be returned.
* @return ConduitClient|null Client, or `null` for local repositories.
*/
public function newConduitClient(
PhabricatorUser $viewer,
$never_proxy = false) {
$uri = $this->getAlmanacServiceURI(
$viewer,
array(
'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;
}
$domain = id(new PhutilURI(PhabricatorEnv::getURI('/')))->getDomain();
$client = id(new ConduitClient($uri))
->setHost($domain);
if ($viewer->isOmnipotent()) {
// If the caller is the omnipotent user (normally, a daemon), we will
// sign the request with this host's asymmetric keypair.
$public_path = AlmanacKeys::getKeyPath('device.pub');
try {
$public_key = Filesystem::readFile($public_path);
} catch (Exception $ex) {
throw new PhutilAggregateException(
pht(
'Unable to read device public key while attempting to make '.
'authenticated method call within the Phabricator cluster. '.
'Use `%s` to register keys for this device. Exception: %s',
'bin/almanac register',
$ex->getMessage()),
array($ex));
}
$private_path = AlmanacKeys::getKeyPath('device.key');
try {
$private_key = Filesystem::readFile($private_path);
$private_key = new PhutilOpaqueEnvelope($private_key);
} catch (Exception $ex) {
throw new PhutilAggregateException(
pht(
'Unable to read device private key while attempting to make '.
'authenticated method call within the Phabricator cluster. '.
'Use `%s` to register keys for this device. Exception: %s',
'bin/almanac register',
$ex->getMessage()),
array($ex));
}
$client->setSigningKeys($public_key, $private_key);
} else {
// If the caller is a normal user, we generate or retrieve a cluster
// API token.
$token = PhabricatorConduitToken::loadClusterTokenForUser($viewer);
if ($token) {
$client->setConduitToken($token->getToken());
}
}
return $client;
}
public function getPassthroughEnvironmentalVariables() {
$env = $_ENV;
if ($this->isGit()) {
// $_ENV does not populate in CLI contexts if "E" is missing from
// "variables_order" in PHP config. Currently, we do not require this
// to be configured. Since it may not be, explicitly bring expected Git
// environmental variables into scope. This list is not exhaustive, but
// only lists variables with a known impact on commit hook behavior.
// This can be removed if we later require "E" in "variables_order".
$git_env = array(
'GIT_OBJECT_DIRECTORY',
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
'GIT_QUARANTINE_PATH',
);
foreach ($git_env as $key) {
$value = getenv($key);
if (strlen($value)) {
$env[$key] = $value;
}
}
$key = 'GIT_PUSH_OPTION_COUNT';
$git_count = getenv($key);
if (strlen($git_count)) {
$git_count = (int)$git_count;
$env[$key] = $git_count;
for ($ii = 0; $ii < $git_count; $ii++) {
$key = 'GIT_PUSH_OPTION_'.$ii;
$env[$key] = getenv($key);
}
}
}
$result = array();
foreach ($env as $key => $value) {
// In Git, pass anything matching "GIT_*" though. Some of these variables
// need to be preserved to allow `git` operations to work properly when
// running from commit hooks.
if ($this->isGit()) {
if (preg_match('/^GIT_/', $key)) {
$result[$key] = $value;
}
}
}
return $result;
}
public function supportsBranchComparison() {
return $this->isGit();
}
/* -( Repository URIs )---------------------------------------------------- */
public function attachURIs(array $uris) {
$custom_map = array();
foreach ($uris as $key => $uri) {
$builtin_key = $uri->getRepositoryURIBuiltinKey();
if ($builtin_key !== null) {
$custom_map[$builtin_key] = $key;
}
}
$builtin_uris = $this->newBuiltinURIs();
$seen_builtins = array();
foreach ($builtin_uris as $builtin_uri) {
$builtin_key = $builtin_uri->getRepositoryURIBuiltinKey();
$seen_builtins[$builtin_key] = true;
// If this builtin URI is disabled, don't attach it and remove the
// persisted version if it exists.
if ($builtin_uri->getIsDisabled()) {
if (isset($custom_map[$builtin_key])) {
unset($uris[$custom_map[$builtin_key]]);
}
continue;
}
// If the URI exists, make sure it's marked as not being disabled.
if (isset($custom_map[$builtin_key])) {
$uris[$custom_map[$builtin_key]]->setIsDisabled(false);
}
}
// Remove any builtins which no longer exist.
foreach ($custom_map as $builtin_key => $key) {
if (empty($seen_builtins[$builtin_key])) {
unset($uris[$key]);
}
}
$this->uris = $uris;
return $this;
}
public function getURIs() {
return $this->assertAttached($this->uris);
}
public function getCloneURIs() {
$uris = $this->getURIs();
$clone = array();
foreach ($uris as $uri) {
if (!$uri->isBuiltin()) {
continue;
}
if ($uri->getIsDisabled()) {
continue;
}
$io_type = $uri->getEffectiveIoType();
$is_clone =
($io_type == PhabricatorRepositoryURI::IO_READ) ||
($io_type == PhabricatorRepositoryURI::IO_READWRITE);
if (!$is_clone) {
continue;
}
$clone[] = $uri;
}
$clone = msort($clone, 'getURIScore');
$clone = array_reverse($clone);
return $clone;
}
public function newBuiltinURIs() {
$has_callsign = ($this->getCallsign() !== null);
$has_shortname = ($this->getRepositorySlug() !== null);
$identifier_map = array(
PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_CALLSIGN => $has_callsign,
PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_SHORTNAME => $has_shortname,
PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_ID => true,
);
// If the view policy of the repository is public, support anonymous HTTP
// even if authenticated HTTP is not supported.
if ($this->getViewPolicy() === PhabricatorPolicies::POLICY_PUBLIC) {
$allow_http = true;
} else {
$allow_http = PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth');
}
$base_uri = PhabricatorEnv::getURI('/');
$base_uri = new PhutilURI($base_uri);
$has_https = ($base_uri->getProtocol() == 'https');
$has_https = ($has_https && $allow_http);
$has_http = !PhabricatorEnv::getEnvConfig('security.require-https');
$has_http = ($has_http && $allow_http);
// HTTP is not supported for Subversion.
if ($this->isSVN()) {
$has_http = false;
$has_https = false;
}
$has_ssh = (bool)strlen(PhabricatorEnv::getEnvConfig('phd.user'));
$protocol_map = array(
PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH => $has_ssh,
PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTPS => $has_https,
PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTP => $has_http,
);
$uris = array();
foreach ($protocol_map as $protocol => $proto_supported) {
foreach ($identifier_map as $identifier => $id_supported) {
// This is just a dummy value because it can't be empty; we'll force
// it to a proper value when using it in the UI.
$builtin_uri = "{$protocol}://{$identifier}";
$uris[] = PhabricatorRepositoryURI::initializeNewURI()
->setRepositoryPHID($this->getPHID())
->attachRepository($this)
->setBuiltinProtocol($protocol)
->setBuiltinIdentifier($identifier)
->setURI($builtin_uri)
->setIsDisabled((int)(!$proto_supported || !$id_supported));
}
}
return $uris;
}
public function getClusterRepositoryURIFromBinding(
AlmanacBinding $binding) {
$protocol = $binding->getAlmanacPropertyValue('protocol');
if ($protocol === null) {
$protocol = 'https';
}
$iface = $binding->getInterface();
$address = $iface->renderDisplayAddress();
$path = $this->getURI();
return id(new PhutilURI("{$protocol}://{$address}"))
->setPath($path);
}
public function loadAlmanacService() {
$service_phid = $this->getAlmanacServicePHID();
if (!$service_phid) {
// No service, so this is a local repository.
return null;
}
$service = id(new AlmanacServiceQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($service_phid))
->needBindings(true)
->needProperties(true)
->executeOne();
if (!$service) {
throw new Exception(
pht(
'The Almanac service for this repository is invalid or could not '.
'be loaded.'));
}
$service_type = $service->getServiceImplementation();
if (!($service_type instanceof AlmanacClusterRepositoryServiceType)) {
throw new Exception(
pht(
'The Almanac service for this repository does not have the correct '.
'service type.'));
}
return $service;
}
public function markImporting() {
$this->openTransaction();
$this->beginReadLocking();
$repository = $this->reload();
$repository->setDetail('importing', true);
$repository->save();
$this->endReadLocking();
$this->saveTransaction();
return $repository;
}
/* -( Symbols )-------------------------------------------------------------*/
public function getSymbolSources() {
return $this->getDetail('symbol-sources', array());
}
public function getSymbolLanguages() {
return $this->getDetail('symbol-languages', array());
}
/* -( Staging )------------------------------------------------------------ */
public function supportsStaging() {
return $this->isGit();
}
public function getStagingURI() {
if (!$this->supportsStaging()) {
return null;
}
return $this->getDetail('staging-uri', null);
}
/* -( Automation )--------------------------------------------------------- */
public function supportsAutomation() {
return $this->isGit();
}
public function canPerformAutomation() {
if (!$this->supportsAutomation()) {
return false;
}
if (!$this->getAutomationBlueprintPHIDs()) {
return false;
}
return true;
}
public function getAutomationBlueprintPHIDs() {
if (!$this->supportsAutomation()) {
return array();
}
return $this->getDetail('automation.blueprintPHIDs', array());
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorRepositoryEditor();
}
public function getApplicationTransactionTemplate() {
return new PhabricatorRepositoryTransaction();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
DiffusionPushCapability::CAPABILITY,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
case DiffusionPushCapability::CAPABILITY:
return $this->getPushPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
return false;
}
/* -( PhabricatorMarkupInterface )----------------------------------------- */
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digestForIndex($this->getMarkupText($field));
return "repo:{$hash}";
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newMarkupEngine(array());
}
public function getMarkupText($field) {
return $this->getDetail('description');
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
require_celerity_resource('phabricator-remarkup-css');
return phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$output);
}
public function shouldUseMarkupCache($field) {
return true;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$phid = $this->getPHID();
$this->openTransaction();
$this->delete();
PhabricatorRepositoryURIIndex::updateRepositoryURIs($phid, array());
$books = id(new DivinerBookQuery())
->setViewer($engine->getViewer())
->withRepositoryPHIDs(array($phid))
->execute();
foreach ($books as $book) {
$engine->destroyObject($book);
}
$atoms = id(new DivinerAtomQuery())
->setViewer($engine->getViewer())
->withRepositoryPHIDs(array($phid))
->execute();
foreach ($atoms as $atom) {
$engine->destroyObject($atom);
}
$lfs_refs = id(new PhabricatorRepositoryGitLFSRefQuery())
->setViewer($engine->getViewer())
->withRepositoryPHIDs(array($phid))
->execute();
foreach ($lfs_refs as $ref) {
$engine->destroyObject($ref);
}
$this->saveTransaction();
}
/* -( PhabricatorDestructibleCodexInterface )------------------------------ */
public function newDestructibleCodex() {
return new PhabricatorRepositoryDestructibleCodex();
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
return $this->spacePHID;
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The repository name.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('vcs')
->setType('string')
->setDescription(
pht('The VCS this repository uses ("git", "hg" or "svn").')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('callsign')
->setType('string')
->setDescription(pht('The repository callsign, if it has one.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('shortName')
->setType('string')
->setDescription(pht('Unique short name, if the repository has one.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('status')
->setType('string')
->setDescription(pht('Active or inactive status.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('isImporting')
->setType('bool')
->setDescription(
pht(
'True if the repository is importing initial commits.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('almanacServicePHID')
->setType('phid?')
->setDescription(
pht(
'The Almanac Service that hosts this repository, if the '.
'repository is clustered.')),
);
}
public function getFieldValuesForConduit() {
return array(
'name' => $this->getName(),
'vcs' => $this->getVersionControlSystem(),
'callsign' => $this->getCallsign(),
'shortName' => $this->getRepositorySlug(),
'status' => $this->getStatus(),
'isImporting' => (bool)$this->isImporting(),
'almanacServicePHID' => $this->getAlmanacServicePHID(),
);
}
public function getConduitSearchAttachments() {
return array(
id(new DiffusionRepositoryURIsSearchEngineAttachment())
->setAttachmentKey('uris'),
);
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhabricatorRepositoryFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new PhabricatorRepositoryFerretEngine();
}
}
diff --git a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php
index 76c6aed9e..e3833bd10 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php
@@ -1,141 +1,130 @@
<?php
final class PhabricatorRepositoryIdentity
extends PhabricatorRepositoryDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface {
protected $authorPHID;
protected $identityNameHash;
protected $identityNameRaw;
protected $identityNameEncoding;
protected $automaticGuessedUserPHID;
protected $manuallySetUserPHID;
protected $currentEffectiveUserPHID;
- private $effectiveUser = self::ATTACHABLE;
-
- public function attachEffectiveUser(PhabricatorUser $user) {
- $this->effectiveUser = $user;
- return $this;
- }
-
- public function getEffectiveUser() {
- return $this->assertAttached($this->effectiveUser);
- }
-
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_BINARY => array(
'identityNameRaw' => true,
),
self::CONFIG_COLUMN_SCHEMA => array(
'identityNameHash' => 'bytes12',
'identityNameEncoding' => 'text16?',
'automaticGuessedUserPHID' => 'phid?',
'manuallySetUserPHID' => 'phid?',
'currentEffectiveUserPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_identity' => array(
'columns' => array('identityNameHash'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function getPHIDType() {
return PhabricatorRepositoryIdentityPHIDType::TYPECONST;
}
public function setIdentityName($name_raw) {
$this->setIdentityNameRaw($name_raw);
$this->setIdentityNameHash(PhabricatorHash::digestForIndex($name_raw));
$this->setIdentityNameEncoding($this->detectEncodingForStorage($name_raw));
return $this;
}
public function getIdentityName() {
return $this->getUTF8StringFromStorage(
$this->getIdentityNameRaw(),
$this->getIdentityNameEncoding());
}
public function getIdentityEmailAddress() {
$address = new PhutilEmailAddress($this->getIdentityName());
return $address->getAddress();
}
public function getIdentityDisplayName() {
$address = new PhutilEmailAddress($this->getIdentityName());
return $address->getDisplayName();
}
public function getIdentityShortName() {
// TODO
return $this->getIdentityName();
}
public function getURI() {
return '/diffusion/identity/view/'.$this->getID().'/';
}
public function hasEffectiveUser() {
return ($this->currentEffectiveUserPHID != null);
}
public function getIdentityDisplayPHID() {
if ($this->hasEffectiveUser()) {
return $this->getCurrentEffectiveUserPHID();
} else {
return $this->getPHID();
}
}
public function save() {
if ($this->manuallySetUserPHID) {
$this->currentEffectiveUserPHID = $this->manuallySetUserPHID;
} else {
$this->currentEffectiveUserPHID = $this->automaticGuessedUserPHID;
}
return parent::save();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::getMostOpenPolicy();
}
public function hasAutomaticCapability(
$capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new DiffusionRepositoryIdentityEditor();
}
public function getApplicationTransactionTemplate() {
return new PhabricatorRepositoryIdentityTransaction();
}
}
diff --git a/src/applications/repository/storage/PhabricatorRepositoryTransaction.php b/src/applications/repository/storage/PhabricatorRepositoryTransaction.php
index 85c354ba6..8729e462a 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryTransaction.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryTransaction.php
@@ -1,22 +1,18 @@
<?php
final class PhabricatorRepositoryTransaction
extends PhabricatorModularTransaction {
public function getApplicationName() {
return 'repository';
}
public function getApplicationTransactionType() {
return PhabricatorRepositoryRepositoryPHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return null;
- }
-
public function getBaseTransactionClass() {
return 'PhabricatorRepositoryTransactionType';
}
}
diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php
index 75ae0c9c1..d5054a7f1 100644
--- a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php
+++ b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php
@@ -1,193 +1,255 @@
<?php
final class PhabricatorRepositoryCommitOwnersWorker
extends PhabricatorRepositoryCommitParserWorker {
protected function getImportStepFlag() {
return PhabricatorRepositoryCommit::IMPORTED_OWNERS;
}
protected function parseCommit(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
if (!$this->shouldSkipImportStep()) {
$this->triggerOwnerAudits($repository, $commit);
$commit->writeImportStatusFlag($this->getImportStepFlag());
}
if ($this->shouldQueueFollowupTasks()) {
$this->queueTask(
'PhabricatorRepositoryCommitHeraldWorker',
array(
'commitID' => $commit->getID(),
));
}
}
private function triggerOwnerAudits(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
$viewer = PhabricatorUser::getOmnipotentUser();
if (!$repository->shouldPublish()) {
return;
}
$affected_paths = PhabricatorOwnerPathQuery::loadAffectedPaths(
$repository,
$commit,
PhabricatorUser::getOmnipotentUser());
$affected_packages = PhabricatorOwnersPackage::loadAffectedPackages(
$repository,
$affected_paths);
$commit->writeOwnersEdges(mpull($affected_packages, 'getPHID'));
if (!$affected_packages) {
return;
}
$commit = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withPHIDs(array($commit->getPHID()))
->needCommitData(true)
->needAuditRequests(true)
->executeOne();
if (!$commit) {
return;
}
$data = $commit->getCommitData();
$author_phid = $data->getCommitDetail('authorPHID');
$revision_id = $data->getCommitDetail('differential.revisionID');
if ($revision_id) {
$revision = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($revision_id))
->needReviewers(true)
->executeOne();
} else {
$revision = null;
}
$requests = $commit->getAudits();
$requests = mpull($requests, null, 'getAuditorPHID');
$auditor_phids = array();
foreach ($affected_packages as $package) {
$request = idx($requests, $package->getPHID());
if ($request) {
// Don't update request if it exists already.
continue;
}
$should_audit = $this->shouldTriggerAudit(
$commit,
$package,
$author_phid,
$revision);
if (!$should_audit) {
continue;
}
$auditor_phids[] = $package->getPHID();
}
// If none of the packages are triggering audits, we're all done.
if (!$auditor_phids) {
return;
}
$audit_type = DiffusionCommitAuditorsTransaction::TRANSACTIONTYPE;
$owners_phid = id(new PhabricatorOwnersApplication())
->getPHID();
$content_source = $this->newContentSource();
$xactions = array();
$xactions[] = $commit->getApplicationTransactionTemplate()
->setTransactionType($audit_type)
->setNewValue(
array(
'+' => array_fuse($auditor_phids),
));
$editor = $commit->getApplicationTransactionEditor()
->setActor($viewer)
->setActingAsPHID($owners_phid)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setContentSource($content_source);
$editor->applyTransactions($commit, $xactions);
}
private function shouldTriggerAudit(
PhabricatorRepositoryCommit $commit,
PhabricatorOwnersPackage $package,
$author_phid,
$revision) {
- // Don't trigger an audit if auditing isn't enabled for the package.
- if (!$package->getAuditingEnabled()) {
- return false;
+ $audit_uninvolved = false;
+ $audit_unreviewed = false;
+
+ $rule = $package->newAuditingRule();
+ switch ($rule->getKey()) {
+ case PhabricatorOwnersAuditRule::AUDITING_NONE:
+ return false;
+ case PhabricatorOwnersAuditRule::AUDITING_ALL:
+ return true;
+ case PhabricatorOwnersAuditRule::AUDITING_NO_OWNER:
+ $audit_uninvolved = true;
+ break;
+ case PhabricatorOwnersAuditRule::AUDITING_UNREVIEWED:
+ $audit_unreviewed = true;
+ break;
+ case PhabricatorOwnersAuditRule::AUDITING_NO_OWNER_AND_UNREVIEWED:
+ $audit_uninvolved = true;
+ $audit_unreviewed = true;
+ break;
}
- // Trigger an audit if we don't recognize the commit's author.
- if (!$author_phid) {
- return true;
+ // If auditing is configured to trigger on unreviewed changes, check if
+ // the revision was "Accepted" when it landed. If not, trigger an audit.
+ if ($audit_unreviewed) {
+ $commit_unreviewed = true;
+ if ($revision) {
+ $was_accepted = DifferentialRevision::PROPERTY_CLOSED_FROM_ACCEPTED;
+ if ($revision->isPublished()) {
+ if ($revision->getProperty($was_accepted)) {
+ $commit_unreviewed = false;
+ }
+ }
+ }
+
+ if ($commit_unreviewed) {
+ return true;
+ }
+ }
+
+ // If auditing is configured to trigger on changes with no involved owner,
+ // check for an owner. If we don't find one, trigger an audit.
+ if ($audit_uninvolved) {
+ $owner_involved = $this->isOwnerInvolved(
+ $commit,
+ $package,
+ $author_phid,
+ $revision);
+ if (!$owner_involved) {
+ return true;
+ }
}
+ // We can't find any reason to trigger an audit for this commit.
+ return false;
+ }
+
+ private function isOwnerInvolved(
+ PhabricatorRepositoryCommit $commit,
+ PhabricatorOwnersPackage $package,
+ $author_phid,
+ $revision) {
+
$owner_phids = PhabricatorOwnersOwner::loadAffiliatedUserPHIDs(
array(
$package->getID(),
));
$owner_phids = array_fuse($owner_phids);
- // Don't trigger an audit if the author is a package owner.
- if (isset($owner_phids[$author_phid])) {
- return false;
+ // For the purposes of deciding whether the owners were involved in the
+ // revision or not, consider a review by the package itself to count as
+ // involvement. This can happen when human reviewers force-accept on
+ // behalf of packages they don't own but have authority over.
+ $owner_phids[$package->getPHID()] = $package->getPHID();
+
+ // If the commit author is identifiable and a package owner, they're
+ // involved.
+ if ($author_phid) {
+ if (isset($owner_phids[$author_phid])) {
+ return true;
+ }
}
- // Trigger an audit of there is no corresponding revision.
+ // Otherwise, we need to find an owner as a reviewer.
+
+ // If we don't have a revision, this is hopeless: no owners are involved.
if (!$revision) {
return true;
}
$accepted_statuses = array(
DifferentialReviewerStatus::STATUS_ACCEPTED,
DifferentialReviewerStatus::STATUS_ACCEPTED_OLDER,
);
$accepted_statuses = array_fuse($accepted_statuses);
$found_accept = false;
foreach ($revision->getReviewers() as $reviewer) {
$reviewer_phid = $reviewer->getReviewerPHID();
- // If this reviewer isn't a package owner, just ignore them.
+ // If this reviewer isn't a package owner or the package itself,
+ // just ignore them.
if (empty($owner_phids[$reviewer_phid])) {
continue;
}
- // If this reviewer accepted the revision and owns the package, we're
- // all clear and do not need to trigger an audit.
+ // If this reviewer accepted the revision and owns the package (or is
+ // the package), we've found an involved owner.
if (isset($accepted_statuses[$reviewer->getReviewerStatus()])) {
$found_accept = true;
break;
}
}
- // Don't trigger an audit if a package owner already reviewed the
- // revision.
if ($found_accept) {
- return false;
+ return true;
}
- return true;
+ return false;
}
}
diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php
index cf4f95c16..4bf4929f4 100644
--- a/src/applications/search/controller/PhabricatorApplicationSearchController.php
+++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php
@@ -1,975 +1,993 @@
<?php
final class PhabricatorApplicationSearchController
extends PhabricatorSearchBaseController {
private $searchEngine;
private $navigation;
private $queryKey;
private $preface;
private $activeQuery;
public function setPreface($preface) {
$this->preface = $preface;
return $this;
}
public function getPreface() {
return $this->preface;
}
public function setQueryKey($query_key) {
$this->queryKey = $query_key;
return $this;
}
protected function getQueryKey() {
return $this->queryKey;
}
public function setNavigation(AphrontSideNavFilterView $navigation) {
$this->navigation = $navigation;
return $this;
}
protected function getNavigation() {
return $this->navigation;
}
public function setSearchEngine(
PhabricatorApplicationSearchEngine $search_engine) {
$this->searchEngine = $search_engine;
return $this;
}
protected function getSearchEngine() {
return $this->searchEngine;
}
protected function getActiveQuery() {
if (!$this->activeQuery) {
throw new Exception(pht('There is no active query yet.'));
}
return $this->activeQuery;
}
protected function validateDelegatingController() {
$parent = $this->getDelegatingController();
if (!$parent) {
throw new Exception(
pht('You must delegate to this controller, not invoke it directly.'));
}
$engine = $this->getSearchEngine();
if (!$engine) {
throw new PhutilInvalidStateException('setEngine');
}
$engine->setViewer($this->getRequest()->getUser());
$parent = $this->getDelegatingController();
}
public function processRequest() {
$this->validateDelegatingController();
$query_action = $this->getRequest()->getURIData('queryAction');
if ($query_action == 'export') {
return $this->processExportRequest();
}
$key = $this->getQueryKey();
if ($key == 'edit') {
return $this->processEditRequest();
} else {
return $this->processSearchRequest();
}
}
private function processSearchRequest() {
$parent = $this->getDelegatingController();
$request = $this->getRequest();
$user = $request->getUser();
$engine = $this->getSearchEngine();
$nav = $this->getNavigation();
if (!$nav) {
$nav = $this->buildNavigation();
}
if ($request->isFormPost()) {
$saved_query = $engine->buildSavedQueryFromRequest($request);
$engine->saveQuery($saved_query);
return id(new AphrontRedirectResponse())->setURI(
$engine->getQueryResultsPageURI($saved_query->getQueryKey()).'#R');
}
$named_query = null;
$run_query = true;
$query_key = $this->queryKey;
if ($this->queryKey == 'advanced') {
$run_query = false;
$query_key = $request->getStr('query');
} else if (!strlen($this->queryKey)) {
$found_query_data = false;
if ($request->isHTTPGet() || $request->isQuicksand()) {
// If this is a GET request and it has some query data, don't
// do anything unless it's only before= or after=. We'll build and
// execute a query from it below. This allows external tools to build
// URIs like "/query/?users=a,b".
$pt_data = $request->getPassthroughRequestData();
$exempt = array(
'before' => true,
'after' => true,
'nux' => true,
'overheated' => true,
);
foreach ($pt_data as $pt_key => $pt_value) {
if (isset($exempt[$pt_key])) {
continue;
}
$found_query_data = true;
break;
}
}
if (!$found_query_data) {
// Otherwise, there's no query data so just run the user's default
// query for this application.
$query_key = $engine->getDefaultQueryKey();
}
}
if ($engine->isBuiltinQuery($query_key)) {
$saved_query = $engine->buildSavedQueryFromBuiltin($query_key);
$named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
} else if ($query_key) {
$saved_query = id(new PhabricatorSavedQueryQuery())
->setViewer($user)
->withQueryKeys(array($query_key))
->executeOne();
if (!$saved_query) {
return new Aphront404Response();
}
$named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
} else {
$saved_query = $engine->buildSavedQueryFromRequest($request);
// Save the query to generate a query key, so "Save Custom Query..." and
// other features like "Bulk Edit" and "Export Data" work correctly.
$engine->saveQuery($saved_query);
}
$this->activeQuery = $saved_query;
$nav->selectFilter(
'query/'.$saved_query->getQueryKey(),
'query/advanced');
$form = id(new AphrontFormView())
->setUser($user)
->setAction($request->getPath());
$engine->buildSearchForm($form, $saved_query);
$errors = $engine->getErrors();
if ($errors) {
$run_query = false;
}
$submit = id(new AphrontFormSubmitControl())
->setValue(pht('Search'));
if ($run_query && !$named_query && $user->isLoggedIn()) {
$save_button = id(new PHUIButtonView())
->setTag('a')
->setHref('/search/edit/key/'.$saved_query->getQueryKey().'/')
->setText(pht('Save Query'))
->setIcon('fa-floppy-o');
$submit->addButton($save_button);
}
// TODO: A "Create Dashboard Panel" action goes here somewhere once
// we sort out T5307.
$form->appendChild($submit);
$body = array();
if ($this->getPreface()) {
$body[] = $this->getPreface();
}
if ($named_query) {
$title = $named_query->getQueryName();
} else {
$title = pht('Advanced Search');
}
$header = id(new PHUIHeaderView())
->setHeader($title)
->setProfileHeader(true);
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->addClass('application-search-results');
if ($run_query || $named_query) {
$box->setShowHide(
pht('Edit Query'),
pht('Hide Query'),
$form,
$this->getApplicationURI('query/advanced/?query='.$query_key),
(!$named_query ? true : false));
} else {
$box->setForm($form);
}
$body[] = $box;
$more_crumbs = null;
if ($run_query) {
$exec_errors = array();
$box->setAnchor(
id(new PhabricatorAnchorView())
->setAnchorName('R'));
try {
$engine->setRequest($request);
$query = $engine->buildQueryFromSavedQuery($saved_query);
$pager = $engine->newPagerForSavedQuery($saved_query);
$pager->readFromRequest($request);
+ $query->setReturnPartialResultsOnOverheat(true);
+
$objects = $engine->executeQuery($query, $pager);
$force_nux = $request->getBool('nux');
if (!$objects || $force_nux) {
$nux_view = $this->renderNewUserView($engine, $force_nux);
} else {
$nux_view = null;
}
$is_overflowing =
$pager->willShowPagingControls() &&
$engine->getResultBucket($saved_query);
$force_overheated = $request->getBool('overheated');
$is_overheated = $query->getIsOverheated() || $force_overheated;
if ($nux_view) {
$box->appendChild($nux_view);
} else {
$list = $engine->renderResults($objects, $saved_query);
if (!($list instanceof PhabricatorApplicationSearchResultView)) {
throw new Exception(
pht(
'SearchEngines must render a "%s" object, but this engine '.
- '(of class "%s") rendered something else.',
+ '(of class "%s") rendered something else ("%s").',
'PhabricatorApplicationSearchResultView',
- get_class($engine)));
+ get_class($engine),
+ phutil_describe_type($list)));
}
if ($list->getObjectList()) {
$box->setObjectList($list->getObjectList());
}
if ($list->getTable()) {
$box->setTable($list->getTable());
}
if ($list->getInfoView()) {
$box->setInfoView($list->getInfoView());
}
if ($is_overflowing) {
$box->appendChild($this->newOverflowingView());
}
if ($list->getContent()) {
$box->appendChild($list->getContent());
}
if ($is_overheated) {
$box->appendChild($this->newOverheatedView($objects));
}
$result_header = $list->getHeader();
if ($result_header) {
$box->setHeader($result_header);
$header = $result_header;
}
$actions = $list->getActions();
if ($actions) {
foreach ($actions as $action) {
$header->addActionLink($action);
}
}
$use_actions = $engine->newUseResultsActions($saved_query);
// TODO: Eventually, modularize all this stuff.
$builtin_use_actions = $this->newBuiltinUseActions();
if ($builtin_use_actions) {
foreach ($builtin_use_actions as $builtin_use_action) {
$use_actions[] = $builtin_use_action;
}
}
if ($use_actions) {
$use_dropdown = $this->newUseResultsDropdown(
$saved_query,
$use_actions);
$header->addActionLink($use_dropdown);
}
$more_crumbs = $list->getCrumbs();
if ($pager->willShowPagingControls()) {
$pager_box = id(new PHUIBoxView())
->setColor(PHUIBoxView::GREY)
->addClass('application-search-pager')
->appendChild($pager);
$body[] = $pager_box;
}
}
} catch (PhabricatorTypeaheadInvalidTokenException $ex) {
$exec_errors[] = pht(
'This query specifies an invalid parameter. Review the '.
'query parameters and correct errors.');
} catch (PhutilSearchQueryCompilerSyntaxException $ex) {
$exec_errors[] = $ex->getMessage();
} catch (PhabricatorSearchConstraintException $ex) {
$exec_errors[] = $ex->getMessage();
+ } catch (PhabricatorInvalidQueryCursorException $ex) {
+ $exec_errors[] = $ex->getMessage();
}
// The engine may have encountered additional errors during rendering;
// merge them in and show everything.
foreach ($engine->getErrors() as $error) {
$exec_errors[] = $error;
}
$errors = $exec_errors;
}
if ($errors) {
$box->setFormErrors($errors, pht('Query Errors'));
}
$crumbs = $parent
->buildApplicationCrumbs()
->setBorder(true);
if ($more_crumbs) {
$query_uri = $engine->getQueryResultsPageURI($saved_query->getQueryKey());
$crumbs->addTextCrumb($title, $query_uri);
foreach ($more_crumbs as $crumb) {
$crumbs->addCrumb($crumb);
}
} else {
$crumbs->addTextCrumb($title);
}
require_celerity_resource('application-search-view-css');
return $this->newPage()
->setApplicationMenu($this->buildApplicationMenu())
->setTitle(pht('Query: %s', $title))
->setCrumbs($crumbs)
->setNavigation($nav)
->addClass('application-search-view')
->appendChild($body);
}
private function processExportRequest() {
$viewer = $this->getViewer();
$engine = $this->getSearchEngine();
$request = $this->getRequest();
if (!$this->canExport()) {
return new Aphront404Response();
}
$query_key = $this->getQueryKey();
if ($engine->isBuiltinQuery($query_key)) {
$saved_query = $engine->buildSavedQueryFromBuiltin($query_key);
} else if ($query_key) {
$saved_query = id(new PhabricatorSavedQueryQuery())
->setViewer($viewer)
->withQueryKeys(array($query_key))
->executeOne();
} else {
$saved_query = null;
}
if (!$saved_query) {
return new Aphront404Response();
}
$cancel_uri = $engine->getQueryResultsPageURI($query_key);
$named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
if ($named_query) {
$filename = $named_query->getQueryName();
$sheet_title = $named_query->getQueryName();
} else {
$filename = $engine->getResultTypeDescription();
$sheet_title = $engine->getResultTypeDescription();
}
$filename = phutil_utf8_strtolower($filename);
$filename = PhabricatorFile::normalizeFileName($filename);
$all_formats = PhabricatorExportFormat::getAllExportFormats();
$available_options = array();
$unavailable_options = array();
$formats = array();
$unavailable_formats = array();
foreach ($all_formats as $key => $format) {
if ($format->isExportFormatEnabled()) {
$available_options[$key] = $format->getExportFormatName();
$formats[$key] = $format;
} else {
$unavailable_options[$key] = pht(
'%s (Not Available)',
$format->getExportFormatName());
$unavailable_formats[$key] = $format;
}
}
$format_options = $available_options + $unavailable_options;
// Try to default to the format the user used last time. If you just
// exported to Excel, you probably want to export to Excel again.
$format_key = $this->readExportFormatPreference();
if (!isset($formats[$format_key])) {
$format_key = head_key($format_options);
}
// Check if this is a large result set or not. If we're exporting a
// large amount of data, we'll build the actual export file in the daemons.
$threshold = 1000;
$query = $engine->buildQueryFromSavedQuery($saved_query);
$pager = $engine->newPagerForSavedQuery($saved_query);
$pager->setPageSize($threshold + 1);
$objects = $engine->executeQuery($query, $pager);
$object_count = count($objects);
$is_large_export = ($object_count > $threshold);
$errors = array();
$e_format = null;
if ($request->isFormPost()) {
$format_key = $request->getStr('format');
if (isset($unavailable_formats[$format_key])) {
$unavailable = $unavailable_formats[$format_key];
$instructions = $unavailable->getInstallInstructions();
$markup = id(new PHUIRemarkupView($viewer, $instructions))
->setRemarkupOption(
PHUIRemarkupView::OPTION_PRESERVE_LINEBREAKS,
false);
return $this->newDialog()
->setTitle(pht('Export Format Not Available'))
->appendChild($markup)
->addCancelButton($cancel_uri, pht('Done'));
}
$format = idx($formats, $format_key);
if (!$format) {
$e_format = pht('Invalid');
$errors[] = pht('Choose a valid export format.');
}
if (!$errors) {
$this->writeExportFormatPreference($format_key);
$export_engine = id(new PhabricatorExportEngine())
->setViewer($viewer)
->setSearchEngine($engine)
->setSavedQuery($saved_query)
->setTitle($sheet_title)
->setFilename($filename)
->setExportFormat($format);
if ($is_large_export) {
$job = $export_engine->newBulkJob($request);
return id(new AphrontRedirectResponse())
->setURI($job->getMonitorURI());
} else {
$file = $export_engine->exportFile();
return $file->newDownloadResponse();
}
}
}
$export_form = id(new AphrontFormView())
->setViewer($viewer)
->appendControl(
id(new AphrontFormSelectControl())
->setName('format')
->setLabel(pht('Format'))
->setError($e_format)
->setValue($format_key)
->setOptions($format_options));
if ($is_large_export) {
$submit_button = pht('Continue');
} else {
$submit_button = pht('Download Data');
}
return $this->newDialog()
->setTitle(pht('Export Results'))
->setErrors($errors)
->appendForm($export_form)
->addCancelButton($cancel_uri)
->addSubmitButton($submit_button);
}
private function processEditRequest() {
$parent = $this->getDelegatingController();
$request = $this->getRequest();
$viewer = $request->getUser();
$engine = $this->getSearchEngine();
$nav = $this->getNavigation();
if (!$nav) {
$nav = $this->buildNavigation();
}
$named_queries = $engine->loadAllNamedQueries();
$can_global = $viewer->getIsAdmin();
$groups = array(
'personal' => array(
'name' => pht('Personal Saved Queries'),
'items' => array(),
'edit' => true,
),
'global' => array(
'name' => pht('Global Saved Queries'),
'items' => array(),
'edit' => $can_global,
),
);
foreach ($named_queries as $named_query) {
if ($named_query->isGlobal()) {
$group = 'global';
} else {
$group = 'personal';
}
$groups[$group]['items'][] = $named_query;
}
$default_key = $engine->getDefaultQueryKey();
$lists = array();
foreach ($groups as $group) {
$lists[] = $this->newQueryListView(
$group['name'],
$group['items'],
$default_key,
$group['edit']);
}
$crumbs = $parent
->buildApplicationCrumbs()
->addTextCrumb(pht('Saved Queries'), $engine->getQueryManagementURI())
->setBorder(true);
$nav->selectFilter('query/edit');
$header = id(new PHUIHeaderView())
->setHeader(pht('Saved Queries'))
->setProfileHeader(true);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter($lists);
return $this->newPage()
->setApplicationMenu($this->buildApplicationMenu())
->setTitle(pht('Saved Queries'))
->setCrumbs($crumbs)
->setNavigation($nav)
->appendChild($view);
}
private function newQueryListView(
$list_name,
array $named_queries,
$default_key,
$can_edit) {
$engine = $this->getSearchEngine();
$viewer = $this->getViewer();
$list = id(new PHUIObjectItemListView())
->setViewer($viewer);
if ($can_edit) {
$list_id = celerity_generate_unique_node_id();
$list->setID($list_id);
Javelin::initBehavior(
'search-reorder-queries',
array(
'listID' => $list_id,
'orderURI' => '/search/order/'.get_class($engine).'/',
));
}
foreach ($named_queries as $named_query) {
$class = get_class($engine);
$key = $named_query->getQueryKey();
$item = id(new PHUIObjectItemView())
->setHeader($named_query->getQueryName())
->setHref($engine->getQueryResultsPageURI($key));
if ($named_query->getIsDisabled()) {
if ($can_edit) {
$item->setDisabled(true);
} else {
// If an item is disabled and you don't have permission to edit it,
// just skip it.
continue;
}
}
if ($can_edit) {
if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) {
$icon = 'fa-plus';
$disable_name = pht('Enable');
} else {
$icon = 'fa-times';
if ($named_query->getIsBuiltin()) {
$disable_name = pht('Disable');
} else {
$disable_name = pht('Delete');
}
}
if ($named_query->getID()) {
$disable_href = '/search/delete/id/'.$named_query->getID().'/';
} else {
$disable_href = '/search/delete/key/'.$key.'/'.$class.'/';
}
$item->addAction(
id(new PHUIListItemView())
->setIcon($icon)
->setHref($disable_href)
->setRenderNameAsTooltip(true)
->setName($disable_name)
->setWorkflow(true));
}
$default_disabled = $named_query->getIsDisabled();
$default_icon = 'fa-thumb-tack';
if ($default_key === $key) {
$default_color = 'green';
} else {
$default_color = null;
}
$item->addAction(
id(new PHUIListItemView())
->setIcon("{$default_icon} {$default_color}")
->setHref('/search/default/'.$key.'/'.$class.'/')
->setRenderNameAsTooltip(true)
->setName(pht('Make Default'))
->setWorkflow(true)
->setDisabled($default_disabled));
if ($can_edit) {
if ($named_query->getIsBuiltin()) {
$edit_icon = 'fa-lock lightgreytext';
$edit_disabled = true;
$edit_name = pht('Builtin');
$edit_href = null;
} else {
$edit_icon = 'fa-pencil';
$edit_disabled = false;
$edit_name = pht('Edit');
$edit_href = '/search/edit/id/'.$named_query->getID().'/';
}
$item->addAction(
id(new PHUIListItemView())
->setIcon($edit_icon)
->setHref($edit_href)
->setRenderNameAsTooltip(true)
->setName($edit_name)
->setDisabled($edit_disabled));
}
$item->setGrippable($can_edit);
$item->addSigil('named-query');
$item->setMetadata(
array(
'queryKey' => $named_query->getQueryKey(),
));
$list->addItem($item);
}
$list->setNoDataString(pht('No saved queries.'));
return id(new PHUIObjectBoxView())
->setHeaderText($list_name)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList($list);
}
public function buildApplicationMenu() {
$menu = $this->getDelegatingController()
->buildApplicationMenu();
if ($menu instanceof PHUIApplicationMenuView) {
$menu->setSearchEngine($this->getSearchEngine());
}
return $menu;
}
private function buildNavigation() {
$viewer = $this->getViewer();
$engine = $this->getSearchEngine();
$nav = id(new AphrontSideNavFilterView())
->setUser($viewer)
->setBaseURI(new PhutilURI($this->getApplicationURI()));
$engine->addNavigationItems($nav->getMenu());
return $nav;
}
private function renderNewUserView(
PhabricatorApplicationSearchEngine $engine,
$force_nux) {
// Don't render NUX if the user has clicked away from the default page.
if (strlen($this->getQueryKey())) {
return null;
}
// Don't put NUX in panels because it would be weird.
if ($engine->isPanelContext()) {
return null;
}
// Try to render the view itself first, since this should be very cheap
// (just returning some text).
$nux_view = $engine->renderNewUserView();
if (!$nux_view) {
return null;
}
$query = $engine->newQuery();
if (!$query) {
return null;
}
// Try to load any object at all. If we can, the application has seen some
// use so we just render the normal view.
if (!$force_nux) {
$object = $query
->setViewer(PhabricatorUser::getOmnipotentUser())
->setLimit(1)
+ ->setReturnPartialResultsOnOverheat(true)
->execute();
if ($object) {
return null;
}
}
return $nux_view;
}
private function newUseResultsDropdown(
PhabricatorSavedQuery $query,
array $dropdown_items) {
$viewer = $this->getViewer();
$action_list = id(new PhabricatorActionListView())
->setViewer($viewer);
foreach ($dropdown_items as $dropdown_item) {
$action_list->addAction($dropdown_item);
}
return id(new PHUIButtonView())
->setTag('a')
->setHref('#')
->setText(pht('Use Results'))
->setIcon('fa-bars')
->setDropdownMenu($action_list)
->addClass('dropdown');
}
private function newOverflowingView() {
$message = pht(
'The query matched more than one page of results. Results are '.
'paginated before bucketing, so later pages may contain additional '.
'results in any bucket.');
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setFlush(true)
->setTitle(pht('Buckets Overflowing'))
->setErrors(
array(
$message,
));
}
- private function newOverheatedView(array $results) {
- if ($results) {
+ public static function newOverheatedError($has_results) {
+ $overheated_link = phutil_tag(
+ 'a',
+ array(
+ 'href' => 'https://phurl.io/u/overheated',
+ 'target' => '_blank',
+ ),
+ pht('Learn More'));
+
+ if ($has_results) {
$message = pht(
- 'Most objects matching your query are not visible to you, so '.
- 'filtering results is taking a long time. Only some results are '.
- 'shown. Refine your query to find results more quickly.');
+ 'This query took too long, so only some results are shown. %s',
+ $overheated_link);
} else {
$message = pht(
- 'Most objects matching your query are not visible to you, so '.
- 'filtering results is taking a long time. Refine your query to '.
- 'find results more quickly.');
+ 'This query took too long. %s',
+ $overheated_link);
}
+ return $message;
+ }
+
+ private function newOverheatedView(array $results) {
+ $message = self::newOverheatedError((bool)$results);
+
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setFlush(true)
->setTitle(pht('Query Overheated'))
->setErrors(
array(
$message,
));
}
private function newBuiltinUseActions() {
$actions = array();
$request = $this->getRequest();
$viewer = $request->getUser();
$is_dev = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');
$engine = $this->getSearchEngine();
$engine_class = get_class($engine);
$query_key = $this->getActiveQuery()->getQueryKey();
$can_use = $engine->canUseInPanelContext();
$is_installed = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorDashboardApplication',
$viewer);
if ($can_use && $is_installed) {
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-dashboard')
->setName(pht('Add to Dashboard'))
->setWorkflow(true)
->setHref("/dashboard/panel/install/{$engine_class}/{$query_key}/");
}
if ($this->canExport()) {
$export_uri = $engine->getExportURI($query_key);
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-download')
->setName(pht('Export Data'))
->setWorkflow(true)
->setHref($export_uri);
}
if ($is_dev) {
$engine = $this->getSearchEngine();
$nux_uri = $engine->getQueryBaseURI();
$nux_uri = id(new PhutilURI($nux_uri))
- ->setQueryParam('nux', true);
+ ->replaceQueryParam('nux', true);
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-user-plus')
->setName(pht('DEV: New User State'))
->setHref($nux_uri);
}
if ($is_dev) {
$overheated_uri = $this->getRequest()->getRequestURI()
- ->setQueryParam('overheated', true);
+ ->replaceQueryParam('overheated', true);
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-fire')
->setName(pht('DEV: Overheated State'))
->setHref($overheated_uri);
}
return $actions;
}
private function canExport() {
$engine = $this->getSearchEngine();
if (!$engine->canExport()) {
return false;
}
// Don't allow logged-out users to perform exports. There's no technical
// or policy reason they can't, but we don't normally give them access
// to write files or jobs. For now, just err on the side of caution.
$viewer = $this->getViewer();
if (!$viewer->getPHID()) {
return false;
}
return true;
}
private function readExportFormatPreference() {
$viewer = $this->getViewer();
$export_key = PhabricatorPolicyFavoritesSetting::SETTINGKEY;
return $viewer->getUserSetting($export_key);
}
private function writeExportFormatPreference($value) {
$viewer = $this->getViewer();
$request = $this->getRequest();
if (!$viewer->isLoggedIn()) {
return;
}
$export_key = PhabricatorPolicyFavoritesSetting::SETTINGKEY;
$preferences = PhabricatorUserPreferences::loadUserPreferences($viewer);
$editor = id(new PhabricatorUserPreferencesEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$xactions = array();
$xactions[] = $preferences->newTransaction($export_key, $value);
$editor->applyTransactions($preferences, $xactions);
}
}
diff --git a/src/applications/search/engine/PhabricatorProfileMenuEngine.php b/src/applications/search/engine/PhabricatorProfileMenuEngine.php
index 90f056ac4..d5cb9ee43 100644
--- a/src/applications/search/engine/PhabricatorProfileMenuEngine.php
+++ b/src/applications/search/engine/PhabricatorProfileMenuEngine.php
@@ -1,1325 +1,1323 @@
<?php
abstract class PhabricatorProfileMenuEngine extends Phobject {
private $viewer;
private $profileObject;
private $customPHID;
private $items;
private $defaultItem;
private $controller;
private $navigation;
private $showNavigation = true;
private $editMode;
private $pageClasses = array();
private $showContentCrumbs = true;
const ITEM_CUSTOM_DIVIDER = 'engine.divider';
const ITEM_MANAGE = 'item.configure';
const MODE_COMBINED = 'combined';
const MODE_GLOBAL = 'global';
const MODE_CUSTOM = 'custom';
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setProfileObject($profile_object) {
$this->profileObject = $profile_object;
return $this;
}
public function getProfileObject() {
return $this->profileObject;
}
public function setCustomPHID($custom_phid) {
$this->customPHID = $custom_phid;
return $this;
}
public function getCustomPHID() {
return $this->customPHID;
}
private function getEditModeCustomPHID() {
$mode = $this->getEditMode();
switch ($mode) {
case self::MODE_CUSTOM:
$custom_phid = $this->getCustomPHID();
break;
case self::MODE_GLOBAL:
$custom_phid = null;
break;
}
return $custom_phid;
}
public function setController(PhabricatorController $controller) {
$this->controller = $controller;
return $this;
}
public function getController() {
return $this->controller;
}
private function setDefaultItem(
PhabricatorProfileMenuItemConfiguration $default_item) {
$this->defaultItem = $default_item;
return $this;
}
public function getDefaultItem() {
$this->getItems();
return $this->defaultItem;
}
public function setShowNavigation($show) {
$this->showNavigation = $show;
return $this;
}
public function getShowNavigation() {
return $this->showNavigation;
}
public function addContentPageClass($class) {
$this->pageClasses[] = $class;
return $this;
}
public function setShowContentCrumbs($show_content_crumbs) {
$this->showContentCrumbs = $show_content_crumbs;
return $this;
}
public function getShowContentCrumbs() {
return $this->showContentCrumbs;
}
abstract public function getItemURI($path);
abstract protected function isMenuEngineConfigurable();
abstract protected function getBuiltinProfileItems($object);
protected function getBuiltinCustomProfileItems(
$object,
$custom_phid) {
return array();
}
protected function getEditMode() {
return $this->editMode;
}
public function buildResponse() {
$controller = $this->getController();
$viewer = $controller->getViewer();
$this->setViewer($viewer);
$request = $controller->getRequest();
$item_action = $request->getURIData('itemAction');
if (!$item_action) {
$item_action = 'view';
}
$is_view = ($item_action == 'view');
// If the engine is not configurable, don't respond to any of the editing
// or configuration routes.
if (!$this->isMenuEngineConfigurable()) {
if (!$is_view) {
return new Aphront404Response();
}
}
$item_id = $request->getURIData('itemID');
// If we miss on the MenuEngine route, try the EditEngine route. This will
// be populated while editing items.
if (!$item_id) {
$item_id = $request->getURIData('id');
}
$item_list = $this->getItems();
$selected_item = null;
if (strlen($item_id)) {
$item_id_int = (int)$item_id;
foreach ($item_list as $item) {
if ($item_id_int) {
if ((int)$item->getID() === $item_id_int) {
$selected_item = $item;
break;
}
}
$builtin_key = $item->getBuiltinKey();
if ($builtin_key === (string)$item_id) {
$selected_item = $item;
break;
}
}
}
if (!$selected_item) {
if ($is_view) {
$selected_item = $this->getDefaultItem();
}
}
switch ($item_action) {
case 'view':
case 'info':
case 'hide':
case 'default':
case 'builtin':
if (!$selected_item) {
return new Aphront404Response();
}
break;
case 'edit':
if (!$request->getURIData('id')) {
// If we continue along the "edit" pathway without an ID, we hit an
// unrelated exception because we can not build a new menu item out
// of thin air. For menus, new items are created via the "new"
// action. Just catch this case and 404 early since there's currently
// no clean way to make EditEngine aware of this.
return new Aphront404Response();
}
break;
}
$navigation = $this->buildNavigation();
$crumbs = $controller->buildApplicationCrumbsForEditEngine();
if (!$is_view) {
$navigation->selectFilter(self::ITEM_MANAGE);
if ($selected_item) {
if ($selected_item->getCustomPHID()) {
$edit_mode = 'custom';
} else {
$edit_mode = 'global';
}
} else {
$edit_mode = $request->getURIData('itemEditMode');
}
$available_modes = $this->getViewerEditModes();
if ($available_modes) {
$available_modes = array_fuse($available_modes);
if (isset($available_modes[$edit_mode])) {
$this->editMode = $edit_mode;
} else {
if ($item_action != 'configure') {
return new Aphront404Response();
}
}
}
$page_title = pht('Configure Menu');
} else {
$page_title = $selected_item->getDisplayName();
}
switch ($item_action) {
case 'view':
$navigation->selectFilter($selected_item->getDefaultMenuItemKey());
try {
$content = $this->buildItemViewContent($selected_item);
} catch (Exception $ex) {
$content = id(new PHUIInfoView())
->setTitle(pht('Unable to Render Dashboard'))
->setErrors(array($ex->getMessage()));
}
$crumbs->addTextCrumb($selected_item->getDisplayName());
if (!$content) {
return new Aphront404Response();
}
break;
case 'configure':
$mode = $this->getEditMode();
if (!$mode) {
$crumbs->addTextCrumb(pht('Configure Menu'));
$content = $this->buildMenuEditModeContent();
} else {
if (count($available_modes) > 1) {
$crumbs->addTextCrumb(
pht('Configure Menu'),
$this->getItemURI('configure/'));
switch ($mode) {
case self::MODE_CUSTOM:
$crumbs->addTextCrumb(pht('Personal'));
break;
case self::MODE_GLOBAL:
$crumbs->addTextCrumb(pht('Global'));
break;
}
} else {
$crumbs->addTextCrumb(pht('Configure Menu'));
}
$edit_list = $this->loadItems($mode);
$content = $this->buildItemConfigureContent($edit_list);
}
break;
case 'reorder':
$mode = $this->getEditMode();
$edit_list = $this->loadItems($mode);
$content = $this->buildItemReorderContent($edit_list);
break;
case 'new':
$item_key = $request->getURIData('itemKey');
$mode = $this->getEditMode();
$content = $this->buildItemNewContent($item_key, $mode);
break;
case 'builtin':
$content = $this->buildItemBuiltinContent($selected_item);
break;
case 'hide':
$content = $this->buildItemHideContent($selected_item);
break;
case 'default':
if (!$this->isMenuEnginePinnable()) {
return new Aphront404Response();
}
$content = $this->buildItemDefaultContent(
$selected_item,
$item_list);
break;
case 'edit':
$content = $this->buildItemEditContent();
break;
default:
throw new Exception(
pht(
'Unsupported item action "%s".',
$item_action));
}
if ($content instanceof AphrontResponse) {
return $content;
}
if ($content instanceof AphrontResponseProducerInterface) {
return $content;
}
$crumbs->setBorder(true);
$page = $controller->newPage()
->setTitle($page_title)
->appendChild($content);
if (!$is_view || $this->getShowContentCrumbs()) {
$page->setCrumbs($crumbs);
}
if ($this->getShowNavigation()) {
$page->setNavigation($navigation);
}
if ($is_view) {
foreach ($this->pageClasses as $class) {
$page->addClass($class);
}
}
return $page;
}
public function buildNavigation() {
if ($this->navigation) {
return $this->navigation;
}
$nav = id(new AphrontSideNavFilterView())
->setIsProfileMenu(true)
->setBaseURI(new PhutilURI($this->getItemURI('')));
$menu_items = $this->getItems();
$filtered_items = array();
foreach ($menu_items as $menu_item) {
if ($menu_item->isDisabled()) {
continue;
}
$filtered_items[] = $menu_item;
}
$filtered_groups = mgroup($filtered_items, 'getMenuItemKey');
foreach ($filtered_groups as $group) {
$first_item = head($group);
$first_item->willBuildNavigationItems($group);
}
foreach ($menu_items as $menu_item) {
if ($menu_item->isDisabled()) {
continue;
}
$items = $menu_item->buildNavigationMenuItems();
foreach ($items as $item) {
$this->validateNavigationMenuItem($item);
}
// If the item produced only a single item which does not otherwise
// have a key, try to automatically assign it a reasonable key. This
// makes selecting the correct item simpler.
if (count($items) == 1) {
$item = head($items);
if ($item->getKey() === null) {
$default_key = $menu_item->getDefaultMenuItemKey();
$item->setKey($default_key);
}
}
foreach ($items as $item) {
$nav->addMenuItem($item);
}
}
$nav->selectFilter(null);
$this->navigation = $nav;
return $this->navigation;
}
private function getItems() {
if ($this->items === null) {
$this->items = $this->loadItems(self::MODE_COMBINED);
}
return $this->items;
}
private function loadItems($mode) {
$viewer = $this->getViewer();
$object = $this->getProfileObject();
$items = $this->loadBuiltinProfileItems($mode);
$query = id(new PhabricatorProfileMenuItemConfigurationQuery())
->setViewer($viewer)
->withProfilePHIDs(array($object->getPHID()));
switch ($mode) {
case self::MODE_GLOBAL:
$query->withCustomPHIDs(array(), true);
break;
case self::MODE_CUSTOM:
$query->withCustomPHIDs(array($this->getCustomPHID()), false);
break;
case self::MODE_COMBINED:
$query->withCustomPHIDs(array($this->getCustomPHID()), true);
break;
}
$stored_items = $query->execute();
foreach ($stored_items as $stored_item) {
$impl = $stored_item->getMenuItem();
$impl->setViewer($viewer);
$impl->setEngine($this);
}
// Merge the stored items into the builtin items. If a builtin item has
// a stored version, replace the defaults with the stored changes.
foreach ($stored_items as $stored_item) {
if (!$stored_item->shouldEnableForObject($object)) {
continue;
}
$builtin_key = $stored_item->getBuiltinKey();
if ($builtin_key !== null) {
// If this builtin actually exists, replace the builtin with the
// stored configuration. Otherwise, we're just going to drop the
// stored config: it corresponds to an out-of-date or uninstalled
// item.
if (isset($items[$builtin_key])) {
$items[$builtin_key] = $stored_item;
} else {
continue;
}
} else {
$items[] = $stored_item;
}
}
$items = $this->arrangeItems($items, $mode);
// Make sure exactly one valid item is marked as default.
$default = null;
$first = null;
foreach ($items as $item) {
if (!$item->canMakeDefault() || $item->isDisabled()) {
continue;
}
// If this engine doesn't support pinning items, don't respect any
// setting which might be present in the database.
if ($this->isMenuEnginePinnable()) {
if ($item->isDefault()) {
$default = $item;
break;
}
}
if ($first === null) {
$first = $item;
}
}
if (!$default) {
$default = $first;
}
if ($default) {
$this->setDefaultItem($default);
}
return $items;
}
private function loadBuiltinProfileItems($mode) {
$object = $this->getProfileObject();
switch ($mode) {
case self::MODE_GLOBAL:
$builtins = $this->getBuiltinProfileItems($object);
break;
case self::MODE_CUSTOM:
$builtins = $this->getBuiltinCustomProfileItems(
$object,
$this->getCustomPHID());
break;
case self::MODE_COMBINED:
$builtins = array();
$builtins[] = $this->getBuiltinCustomProfileItems(
$object,
$this->getCustomPHID());
$builtins[] = $this->getBuiltinProfileItems($object);
$builtins = array_mergev($builtins);
break;
}
$items = PhabricatorProfileMenuItem::getAllMenuItems();
$viewer = $this->getViewer();
$order = 1;
$map = array();
foreach ($builtins as $builtin) {
$builtin_key = $builtin->getBuiltinKey();
if (!$builtin_key) {
throw new Exception(
pht(
'Object produced a builtin item with no builtin item key! '.
'Builtin items must have a unique key.'));
}
if (isset($map[$builtin_key])) {
throw new Exception(
pht(
'Object produced two items with the same builtin key ("%s"). '.
'Each item must have a unique builtin key.',
$builtin_key));
}
$item_key = $builtin->getMenuItemKey();
$item = idx($items, $item_key);
if (!$item) {
throw new Exception(
pht(
'Builtin item ("%s") specifies a bad item key ("%s"); there '.
'is no corresponding item implementation available.',
$builtin_key,
$item_key));
}
$item = clone $item;
$item->setViewer($viewer);
$item->setEngine($this);
$builtin
->setProfilePHID($object->getPHID())
->attachMenuItem($item)
->attachProfileObject($object)
->setMenuItemOrder($order);
if (!$builtin->shouldEnableForObject($object)) {
continue;
}
$map[$builtin_key] = $builtin;
$order++;
}
return $map;
}
private function validateNavigationMenuItem($item) {
if (!($item instanceof PHUIListItemView)) {
throw new Exception(
pht(
'Expected buildNavigationMenuItems() to return a list of '.
'PHUIListItemView objects, but got a surprise.'));
}
}
public function getConfigureURI() {
$mode = $this->getEditMode();
switch ($mode) {
case self::MODE_CUSTOM:
return $this->getItemURI('configure/custom/');
case self::MODE_GLOBAL:
return $this->getItemURI('configure/global/');
}
return $this->getItemURI('configure/');
}
private function buildItemReorderContent(array $items) {
$viewer = $this->getViewer();
$object = $this->getProfileObject();
// If you're reordering global items, you need to be able to edit the
// object the menu appears on. If you're reordering custom items, you only
// need to be able to edit the custom object. Currently, the custom object
// is always the viewing user's own user object.
$custom_phid = $this->getEditModeCustomPHID();
if (!$custom_phid) {
PhabricatorPolicyFilter::requireCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
} else {
$policy_object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($custom_phid))
->executeOne();
if (!$policy_object) {
throw new Exception(
pht(
'Failed to load custom PHID "%s"!',
$custom_phid));
}
PhabricatorPolicyFilter::requireCapability(
$viewer,
$policy_object,
PhabricatorPolicyCapability::CAN_EDIT);
}
$controller = $this->getController();
$request = $controller->getRequest();
$request->validateCSRF();
$order = $request->getStrList('order');
$by_builtin = array();
$by_id = array();
foreach ($items as $key => $item) {
$id = $item->getID();
if ($id) {
$by_id[$id] = $key;
continue;
}
$builtin_key = $item->getBuiltinKey();
if ($builtin_key) {
$by_builtin[$builtin_key] = $key;
continue;
}
}
$key_order = array();
foreach ($order as $order_item) {
if (isset($by_id[$order_item])) {
$key_order[] = $by_id[$order_item];
continue;
}
if (isset($by_builtin[$order_item])) {
$key_order[] = $by_builtin[$order_item];
continue;
}
}
$items = array_select_keys($items, $key_order) + $items;
$type_order =
PhabricatorProfileMenuItemConfigurationTransaction::TYPE_ORDER;
$order = 1;
foreach ($items as $item) {
$xactions = array();
$xactions[] = id(new PhabricatorProfileMenuItemConfigurationTransaction())
->setTransactionType($type_order)
->setNewValue($order);
$editor = id(new PhabricatorProfileMenuEditor())
->setContentSourceFromRequest($request)
->setActor($viewer)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true)
->applyTransactions($item, $xactions);
$order++;
}
return id(new AphrontRedirectResponse())
->setURI($this->getConfigureURI());
}
protected function buildItemViewContent(
PhabricatorProfileMenuItemConfiguration $item) {
return $item->newPageContent();
}
private function getViewerEditModes() {
$modes = array();
$viewer = $this->getViewer();
if ($viewer->isLoggedIn() && $this->isMenuEnginePersonalizable()) {
$modes[] = self::MODE_CUSTOM;
}
$object = $this->getProfileObject();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
if ($can_edit) {
$modes[] = self::MODE_GLOBAL;
}
return $modes;
}
protected function isMenuEnginePersonalizable() {
return true;
}
/**
* Does this engine support pinning items?
*
* Personalizable menus disable pinning by default since it creates a number
* of weird edge cases without providing many benefits for current menus.
*
* @return bool True if items may be pinned as default items.
*/
protected function isMenuEnginePinnable() {
return !$this->isMenuEnginePersonalizable();
}
private function buildMenuEditModeContent() {
$viewer = $this->getViewer();
$modes = $this->getViewerEditModes();
if (!$modes) {
return new Aphront404Response();
}
if (count($modes) == 1) {
$mode = head($modes);
return id(new AphrontRedirectResponse())
->setURI($this->getItemURI("configure/{$mode}/"));
}
$menu = id(new PHUIObjectItemListView())
->setUser($viewer);
$modes = array_fuse($modes);
if (isset($modes['custom'])) {
$menu->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Personal Menu Items'))
->setHref($this->getItemURI('configure/custom/'))
->setImageURI($viewer->getProfileImageURI())
->addAttribute(pht('Edit the menu for your personal account.')));
}
if (isset($modes['global'])) {
$icon = id(new PHUIIconView())
->setIcon('fa-globe')
->setBackground('bg-blue');
$menu->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Global Menu Items'))
->setHref($this->getItemURI('configure/global/'))
->setImageIcon($icon)
->addAttribute(pht('Edit the global default menu for all users.')));
}
$box = id(new PHUIObjectBoxView())
->setObjectList($menu);
$header = id(new PHUIHeaderView())
->setHeader(pht('Manage Menu'))
->setHeaderIcon('fa-list');
return id(new PHUITwoColumnView())
->setHeader($header)
->setFooter($box);
}
private function buildItemConfigureContent(array $items) {
$viewer = $this->getViewer();
$object = $this->getProfileObject();
$filtered_groups = mgroup($items, 'getMenuItemKey');
foreach ($filtered_groups as $group) {
$first_item = head($group);
$first_item->willBuildNavigationItems($group);
}
// Users only need to be able to edit the object which this menu appears
// on if they're editing global menu items. For example, users do not need
// to be able to edit the Favorites application to add new items to the
// Favorites menu.
if (!$this->getCustomPHID()) {
PhabricatorPolicyFilter::requireCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
}
$list_id = celerity_generate_unique_node_id();
$mode = $this->getEditMode();
Javelin::initBehavior(
'reorder-profile-menu-items',
array(
'listID' => $list_id,
'orderURI' => $this->getItemURI("reorder/{$mode}/"),
));
$list = id(new PHUIObjectItemListView())
->setID($list_id)
->setNoDataString(pht('This menu currently has no items.'));
foreach ($items as $item) {
$id = $item->getID();
$builtin_key = $item->getBuiltinKey();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$item,
PhabricatorPolicyCapability::CAN_EDIT);
$view = id(new PHUIObjectItemView());
$name = $item->getDisplayName();
$type = $item->getMenuItemTypeName();
if (!strlen(trim($name))) {
$name = pht('Untitled "%s" Item', $type);
}
$view->setHeader($name);
$view->addAttribute($type);
if ($can_edit) {
$view
->setGrippable(true)
->addSigil('profile-menu-item')
->setMetadata(
array(
'key' => nonempty($id, $builtin_key),
));
if ($id) {
$default_uri = $this->getItemURI("default/{$id}/");
} else {
$default_uri = $this->getItemURI("default/{$builtin_key}/");
}
$default_text = null;
if ($this->isMenuEnginePinnable()) {
if ($item->isDefault()) {
$default_icon = 'fa-thumb-tack green';
$default_text = pht('Current Default');
} else if ($item->canMakeDefault()) {
$default_icon = 'fa-thumb-tack';
$default_text = pht('Make Default');
}
}
if ($default_text !== null) {
$view->addAction(
id(new PHUIListItemView())
->setHref($default_uri)
->setWorkflow(true)
->setName($default_text)
->setIcon($default_icon));
}
if ($id) {
$view->setHref($this->getItemURI("edit/{$id}/"));
$hide_uri = $this->getItemURI("hide/{$id}/");
} else {
$view->setHref($this->getItemURI("builtin/{$builtin_key}/"));
$hide_uri = $this->getItemURI("hide/{$builtin_key}/");
}
if ($item->isDisabled()) {
$hide_icon = 'fa-plus';
$hide_text = pht('Enable');
} else if ($item->getBuiltinKey() !== null) {
$hide_icon = 'fa-times';
$hide_text = pht('Disable');
} else {
$hide_icon = 'fa-times';
$hide_text = pht('Delete');
}
$can_disable = $item->canHideMenuItem();
$view->addAction(
id(new PHUIListItemView())
->setHref($hide_uri)
->setWorkflow(true)
->setDisabled(!$can_disable)
->setName($hide_text)
->setIcon($hide_icon));
}
if ($item->isDisabled()) {
$view->setDisabled(true);
}
$list->addItem($view);
}
- $action_view = id(new PhabricatorActionListView())
- ->setUser($viewer);
-
$item_types = PhabricatorProfileMenuItem::getAllMenuItems();
$object = $this->getProfileObject();
$action_list = id(new PhabricatorActionListView())
->setViewer($viewer);
+ // See T12167. This makes the "Actions" dropdown button show up in the
+ // page header.
+ $action_list->setID(celerity_generate_unique_node_id());
+
$action_list->addAction(
id(new PhabricatorActionView())
->setLabel(true)
->setName(pht('Add New Menu Item...')));
foreach ($item_types as $item_type) {
if (!$item_type->canAddToObject($object)) {
continue;
}
$item_key = $item_type->getMenuItemKey();
$edit_mode = $this->getEditMode();
$action_list->addAction(
id(new PhabricatorActionView())
->setIcon($item_type->getMenuItemTypeIcon())
->setName($item_type->getMenuItemTypeName())
->setHref($this->getItemURI("new/{$edit_mode}/{$item_key}/"))
->setWorkflow(true));
}
$action_list->addAction(
id(new PhabricatorActionView())
->setLabel(true)
->setName(pht('Documentation')));
$doc_link = PhabricatorEnv::getDoclink('Profile Menu User Guide');
$doc_name = pht('Profile Menu User Guide');
$action_list->addAction(
id(new PhabricatorActionView())
->setIcon('fa-book')
->setHref($doc_link)
->setName($doc_name));
$header = id(new PHUIHeaderView())
->setHeader(pht('Menu Items'))
->setHeaderIcon('fa-list');
$box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Current Menu Items'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList($list);
- $panel = id(new PHUICurtainPanelView())
- ->appendChild($action_view);
-
$curtain = id(new PHUICurtainView())
->setViewer($viewer)
->setActionList($action_list);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(
array(
$box,
));
return $view;
}
private function buildItemNewContent($item_key, $mode) {
$item_types = PhabricatorProfileMenuItem::getAllMenuItems();
$item_type = idx($item_types, $item_key);
if (!$item_type) {
return new Aphront404Response();
}
$object = $this->getProfileObject();
if (!$item_type->canAddToObject($object)) {
return new Aphront404Response();
}
$custom_phid = $this->getEditModeCustomPHID();
$configuration = PhabricatorProfileMenuItemConfiguration::initializeNewItem(
$object,
$item_type,
$custom_phid);
$viewer = $this->getViewer();
PhabricatorPolicyFilter::requireCapability(
$viewer,
$configuration,
PhabricatorPolicyCapability::CAN_EDIT);
$controller = $this->getController();
return id(new PhabricatorProfileMenuEditEngine())
->setMenuEngine($this)
->setProfileObject($object)
->setNewMenuItemConfiguration($configuration)
->setCustomPHID($custom_phid)
->setController($controller)
->buildResponse();
}
private function buildItemEditContent() {
$viewer = $this->getViewer();
$object = $this->getProfileObject();
$controller = $this->getController();
$custom_phid = $this->getEditModeCustomPHID();
return id(new PhabricatorProfileMenuEditEngine())
->setMenuEngine($this)
->setProfileObject($object)
->setController($controller)
->setCustomPHID($custom_phid)
->buildResponse();
}
private function buildItemBuiltinContent(
PhabricatorProfileMenuItemConfiguration $configuration) {
// If this builtin item has already been persisted, redirect to the
// edit page.
$id = $configuration->getID();
if ($id) {
return id(new AphrontRedirectResponse())
->setURI($this->getItemURI("edit/{$id}/"));
}
// Otherwise, act like we're creating a new item, we're just starting
// with the builtin template.
$viewer = $this->getViewer();
PhabricatorPolicyFilter::requireCapability(
$viewer,
$configuration,
PhabricatorPolicyCapability::CAN_EDIT);
$object = $this->getProfileObject();
$controller = $this->getController();
$custom_phid = $this->getEditModeCustomPHID();
return id(new PhabricatorProfileMenuEditEngine())
->setIsBuiltin(true)
->setMenuEngine($this)
->setProfileObject($object)
->setNewMenuItemConfiguration($configuration)
->setController($controller)
->setCustomPHID($custom_phid)
->buildResponse();
}
private function buildItemHideContent(
PhabricatorProfileMenuItemConfiguration $configuration) {
$controller = $this->getController();
$request = $controller->getRequest();
$viewer = $this->getViewer();
PhabricatorPolicyFilter::requireCapability(
$viewer,
$configuration,
PhabricatorPolicyCapability::CAN_EDIT);
if (!$configuration->canHideMenuItem()) {
return $controller->newDialog()
->setTitle(pht('Mandatory Item'))
->appendParagraph(
pht('This menu item is very important, and can not be disabled.'))
->addCancelButton($this->getConfigureURI());
}
if ($configuration->getBuiltinKey() === null) {
$new_value = null;
$title = pht('Delete Menu Item');
$body = pht('Delete this menu item?');
$button = pht('Delete Menu Item');
} else if ($configuration->isDisabled()) {
$new_value = PhabricatorProfileMenuItemConfiguration::VISIBILITY_VISIBLE;
$title = pht('Enable Menu Item');
$body = pht(
'Enable this menu item? It will appear in the menu again.');
$button = pht('Enable Menu Item');
} else {
$new_value = PhabricatorProfileMenuItemConfiguration::VISIBILITY_DISABLED;
$title = pht('Disable Menu Item');
$body = pht(
'Disable this menu item? It will no longer appear in the menu, but '.
'you can re-enable it later.');
$button = pht('Disable Menu Item');
}
$v_visibility = $configuration->getVisibility();
if ($request->isFormPost()) {
if ($new_value === null) {
$configuration->delete();
} else {
$type_visibility =
PhabricatorProfileMenuItemConfigurationTransaction::TYPE_VISIBILITY;
$xactions = array();
$xactions[] =
id(new PhabricatorProfileMenuItemConfigurationTransaction())
->setTransactionType($type_visibility)
->setNewValue($new_value);
$editor = id(new PhabricatorProfileMenuEditor())
->setContentSourceFromRequest($request)
->setActor($viewer)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true)
->applyTransactions($configuration, $xactions);
}
return id(new AphrontRedirectResponse())
->setURI($this->getConfigureURI());
}
return $controller->newDialog()
->setTitle($title)
->appendParagraph($body)
->addCancelButton($this->getConfigureURI())
->addSubmitButton($button);
}
private function buildItemDefaultContent(
PhabricatorProfileMenuItemConfiguration $configuration,
array $items) {
$controller = $this->getController();
$request = $controller->getRequest();
$viewer = $this->getViewer();
PhabricatorPolicyFilter::requireCapability(
$viewer,
$configuration,
PhabricatorPolicyCapability::CAN_EDIT);
$done_uri = $this->getConfigureURI();
if (!$configuration->canMakeDefault()) {
return $controller->newDialog()
->setTitle(pht('Not Defaultable'))
->appendParagraph(
pht(
'This item can not be set as the default item. This is usually '.
'because the item has no page of its own, or links to an '.
'external page.'))
->addCancelButton($done_uri);
}
if ($configuration->isDefault()) {
return $controller->newDialog()
->setTitle(pht('Already Default'))
->appendParagraph(
pht(
'This item is already set as the default item for this menu.'))
->addCancelButton($done_uri);
}
if ($request->isFormPost()) {
$key = $configuration->getID();
if (!$key) {
$key = $configuration->getBuiltinKey();
}
$this->adjustDefault($key);
return id(new AphrontRedirectResponse())
->setURI($done_uri);
}
return $controller->newDialog()
->setTitle(pht('Make Default'))
->appendParagraph(
pht(
'Set this item as the default for this menu? Users arriving on '.
'this page will be shown the content of this item by default.'))
->addCancelButton($done_uri)
->addSubmitButton(pht('Make Default'));
}
protected function newItem() {
return PhabricatorProfileMenuItemConfiguration::initializeNewBuiltin();
}
protected function newManageItem() {
return $this->newItem()
->setBuiltinKey(self::ITEM_MANAGE)
->setMenuItemKey(PhabricatorManageProfileMenuItem::MENUITEMKEY);
}
public function adjustDefault($key) {
$controller = $this->getController();
$request = $controller->getRequest();
$viewer = $request->getViewer();
$items = $this->loadItems(self::MODE_COMBINED);
// To adjust the default item, we first change any existing items that
// are marked as defaults to "visible", then make the new default item
// the default.
$default = array();
$visible = array();
foreach ($items as $item) {
$builtin_key = $item->getBuiltinKey();
$id = $item->getID();
$is_target =
(($builtin_key !== null) && ($builtin_key === $key)) ||
(($id !== null) && ((int)$id === (int)$key));
if ($is_target) {
if (!$item->isDefault()) {
$default[] = $item;
}
} else {
if ($item->isDefault()) {
$visible[] = $item;
}
}
}
$type_visibility =
PhabricatorProfileMenuItemConfigurationTransaction::TYPE_VISIBILITY;
$v_visible = PhabricatorProfileMenuItemConfiguration::VISIBILITY_VISIBLE;
$v_default = PhabricatorProfileMenuItemConfiguration::VISIBILITY_DEFAULT;
$apply = array(
array($v_visible, $visible),
array($v_default, $default),
);
foreach ($apply as $group) {
list($value, $items) = $group;
foreach ($items as $item) {
$xactions = array();
$xactions[] =
id(new PhabricatorProfileMenuItemConfigurationTransaction())
->setTransactionType($type_visibility)
->setNewValue($value);
$editor = id(new PhabricatorProfileMenuEditor())
->setContentSourceFromRequest($request)
->setActor($viewer)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true)
->applyTransactions($item, $xactions);
}
}
return $this;
}
private function arrangeItems(array $items, $mode) {
// Sort the items.
$items = msortv($items, 'getSortVector');
$object = $this->getProfileObject();
// If we have some global items and some custom items and are in "combined"
// mode, put a hard-coded divider item between them.
if ($mode == self::MODE_COMBINED) {
$list = array();
$seen_custom = false;
$seen_global = false;
foreach ($items as $item) {
if ($item->getCustomPHID()) {
$seen_custom = true;
} else {
if ($seen_custom && !$seen_global) {
$list[] = $this->newItem()
->setBuiltinKey(self::ITEM_CUSTOM_DIVIDER)
->setMenuItemKey(PhabricatorDividerProfileMenuItem::MENUITEMKEY)
->attachProfileObject($object)
->attachMenuItem(
new PhabricatorDividerProfileMenuItem());
}
$seen_global = true;
}
$list[] = $item;
}
$items = $list;
}
// Normalize keys since callers shouldn't rely on this array being
// partially keyed.
$items = array_values($items);
return $items;
}
}
diff --git a/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php b/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php
index 235d74d6f..f4e2dc918 100644
--- a/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php
+++ b/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php
@@ -1,653 +1,639 @@
<?php
abstract class PhabricatorSearchEngineAPIMethod
extends ConduitAPIMethod {
abstract public function newSearchEngine();
final public function getQueryMaps($query) {
$maps = $this->getCustomQueryMaps($query);
// Make sure we emit empty maps as objects, not lists.
foreach ($maps as $key => $map) {
if (!$map) {
$maps[$key] = (object)$map;
}
}
if (!$maps) {
$maps = (object)$maps;
}
return $maps;
}
protected function getCustomQueryMaps($query) {
return array();
}
public function getApplication() {
$engine = $this->newSearchEngine();
$class = $engine->getApplicationClassName();
return PhabricatorApplication::getByClass($class);
}
final protected function defineParamTypes() {
return array(
'queryKey' => 'optional string',
'constraints' => 'optional map<string, wild>',
'attachments' => 'optional map<string, bool>',
'order' => 'optional order',
) + $this->getPagerParamTypes();
}
final protected function defineReturnType() {
return 'map<string, wild>';
}
final protected function execute(ConduitAPIRequest $request) {
$engine = $this->newSearchEngine()
->setViewer($request->getUser());
return $engine->buildConduitResponse($request, $this);
}
final public function getMethodDescription() {
return pht(
'This is a standard **ApplicationSearch** method which will let you '.
'list, query, or search for objects. For documentation on these '.
'endpoints, see **[[ %s | Conduit API: Using Search Endpoints ]]**.',
PhabricatorEnv::getDoclink('Conduit API: Using Search Endpoints'));
}
final public function getMethodDocumentation() {
$viewer = $this->getViewer();
$engine = $this->newSearchEngine()
->setViewer($viewer);
$query = $engine->newQuery();
$out = array();
$out[] = $this->buildQueriesBox($engine);
$out[] = $this->buildConstraintsBox($engine);
$out[] = $this->buildOrderBox($engine, $query);
$out[] = $this->buildFieldsBox($engine);
$out[] = $this->buildAttachmentsBox($engine);
$out[] = $this->buildPagingBox($engine);
return $out;
}
private function buildQueriesBox(
PhabricatorApplicationSearchEngine $engine) {
$viewer = $this->getViewer();
$info = pht(<<<EOTEXT
You can choose a builtin or saved query as a starting point for filtering
results by selecting it with `queryKey`. If you don't specify a `queryKey`,
the query will start with no constraints.
For example, many applications have builtin queries like `"active"` or
`"open"` to find only active or enabled results. To use a `queryKey`, specify
it like this:
```lang=json, name="Selecting a Builtin Query"
{
...
"queryKey": "active",
...
}
```
The table below shows the keys to use to select builtin queries and your
saved queries, but you can also use **any** query you run via the web UI as a
starting point. You can find the key for a query by examining the URI after
running a normal search.
You can use these keys to select builtin queries and your configured saved
queries:
EOTEXT
);
$named_queries = $engine->loadAllNamedQueries();
$rows = array();
foreach ($named_queries as $named_query) {
$builtin = $named_query->getIsBuiltin()
? pht('Builtin')
: pht('Custom');
$rows[] = array(
$named_query->getQueryKey(),
$named_query->getQueryName(),
$builtin,
);
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Query Key'),
pht('Name'),
pht('Builtin'),
))
->setColumnClasses(
array(
'prewrap',
'pri wide',
null,
));
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Builtin and Saved Queries'))
->setCollapsed(true)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
- ->appendChild($this->buildRemarkup($info))
+ ->appendChild($this->newRemarkupDocumentationView($info))
->appendChild($table);
}
private function buildConstraintsBox(
PhabricatorApplicationSearchEngine $engine) {
$info = pht(<<<EOTEXT
You can apply custom constraints by passing a dictionary in `constraints`.
This will let you search for specific sets of results (for example, you may
want show only results with a certain state, status, or owner).
If you specify both a `queryKey` and `constraints`, the builtin or saved query
will be applied first as a starting point, then any additional values in
`constraints` will be applied, overwriting the defaults from the original query.
Different endpoints support different constraints. The constraints this method
supports are detailed below. As an example, you might specify constraints like
this:
```lang=json, name="Example Custom Constraints"
{
...
"constraints": {
"authors": ["PHID-USER-1111", "PHID-USER-2222"],
"statuses": ["open", "closed"],
...
},
...
}
```
This API endpoint supports these constraints:
EOTEXT
);
$fields = $engine->getSearchFieldsForConduit();
// As a convenience, put these fields at the very top, even if the engine
// specifies and alternate display order for the web UI. These fields are
// very important in the API and nearly useless in the web UI.
$fields = array_select_keys(
$fields,
array('ids', 'phids')) + $fields;
$constant_lists = array();
$rows = array();
foreach ($fields as $field) {
$key = $field->getConduitKey();
$label = $field->getLabel();
$constants = $field->newConduitConstants();
$type_object = $field->getConduitParameterType();
if ($type_object) {
$type = $type_object->getTypeName();
$description = $field->getDescription();
if ($constants) {
$description = array(
$description,
' ',
phutil_tag('em', array(), pht('(See table below.)')),
);
}
} else {
$type = null;
$description = phutil_tag('em', array(), pht('Not supported.'));
}
$rows[] = array(
$key,
$label,
$type,
$description,
);
if ($constants) {
- $constant_lists[] = $this->buildRemarkup(
+ $constant_lists[] = $this->newRemarkupDocumentationView(
pht(
'Constants supported by the `%s` constraint:',
'statuses'));
$constants_rows = array();
foreach ($constants as $constant) {
if ($constant->getIsDeprecated()) {
$icon = id(new PHUIIconView())
->setIcon('fa-exclamation-triangle', 'red');
} else {
$icon = null;
}
$constants_rows[] = array(
$constant->getKey(),
array(
$icon,
' ',
$constant->getValue(),
),
);
}
$constants_table = id(new AphrontTableView($constants_rows))
->setHeaders(
array(
pht('Key'),
pht('Value'),
))
->setColumnClasses(
array(
'mono',
'wide',
));
$constant_lists[] = $constants_table;
}
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Key'),
pht('Label'),
pht('Type'),
pht('Description'),
))
->setColumnClasses(
array(
'prewrap',
'pri',
'prewrap',
'wide',
));
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Custom Query Constraints'))
->setCollapsed(true)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
- ->appendChild($this->buildRemarkup($info))
+ ->appendChild($this->newRemarkupDocumentationView($info))
->appendChild($table)
->appendChild($constant_lists);
}
private function buildOrderBox(
PhabricatorApplicationSearchEngine $engine,
$query) {
$orders_info = pht(<<<EOTEXT
Use `order` to choose an ordering for the results.
Either specify a single key from the builtin orders (these are a set of
meaningful, high-level, human-readable orders) or specify a custom list of
low-level columns.
To use a high-level order, choose a builtin order from the table below
and specify it like this:
```lang=json, name="Choosing a Result Order"
{
...
"order": "newest",
...
}
```
These builtin orders are available:
EOTEXT
);
$orders = $query->getBuiltinOrders();
$rows = array();
foreach ($orders as $key => $order) {
$rows[] = array(
$key,
$order['name'],
implode(', ', $order['vector']),
);
}
$orders_table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Key'),
pht('Description'),
pht('Columns'),
))
->setColumnClasses(
array(
'pri',
'',
'wide',
));
$columns_info = pht(<<<EOTEXT
You can choose a low-level column order instead. To do this, provide a list
of columns instead of a single key. This is an advanced feature.
In a custom column order:
- each column may only be specified once;
- each column may be prefixed with `-` to invert the order;
- the last column must be a unique column, usually `id`; and
- no column other than the last may be unique.
To use a low-level order, choose a sequence of columns and specify them like
this:
```lang=json, name="Using a Custom Order"
{
...
"order": ["color", "-name", "id"],
...
}
```
These low-level columns are available:
EOTEXT
);
$columns = $query->getOrderableColumns();
$rows = array();
foreach ($columns as $key => $column) {
$rows[] = array(
$key,
idx($column, 'unique') ? pht('Yes') : pht('No'),
);
}
$columns_table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Key'),
pht('Unique'),
))
->setColumnClasses(
array(
'pri',
'wide',
));
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Result Ordering'))
->setCollapsed(true)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
- ->appendChild($this->buildRemarkup($orders_info))
+ ->appendChild($this->newRemarkupDocumentationView($orders_info))
->appendChild($orders_table)
- ->appendChild($this->buildRemarkup($columns_info))
+ ->appendChild($this->newRemarkupDocumentationView($columns_info))
->appendChild($columns_table);
}
private function buildFieldsBox(
PhabricatorApplicationSearchEngine $engine) {
$info = pht(<<<EOTEXT
Objects matching your query are returned as a list of dictionaries in the
`data` property of the results. Each dictionary has some metadata and a
-`fields` key, which contains the information abou the object that most callers
+`fields` key, which contains the information about the object that most callers
will be interested in.
For example, the results may look something like this:
```lang=json, name="Example Results"
{
...
"data": [
{
"id": 123,
"phid": "PHID-WXYZ-1111",
"fields": {
"name": "First Example Object",
"authorPHID": "PHID-USER-2222"
}
},
{
"id": 124,
"phid": "PHID-WXYZ-3333",
"fields": {
"name": "Second Example Object",
"authorPHID": "PHID-USER-4444"
}
},
...
]
...
}
```
This result structure is standardized across all search methods, but the
available fields differ from application to application.
These are the fields available on this object type:
EOTEXT
);
$specs = $engine->getAllConduitFieldSpecifications();
$rows = array();
foreach ($specs as $key => $spec) {
$type = $spec->getType();
$description = $spec->getDescription();
$rows[] = array(
$key,
$type,
$description,
);
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Key'),
pht('Type'),
pht('Description'),
))
->setColumnClasses(
array(
'pri',
'mono',
'wide',
));
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Object Fields'))
->setCollapsed(true)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
- ->appendChild($this->buildRemarkup($info))
+ ->appendChild($this->newRemarkupDocumentationView($info))
->appendChild($table);
}
private function buildAttachmentsBox(
PhabricatorApplicationSearchEngine $engine) {
$info = pht(<<<EOTEXT
By default, only basic information about objects is returned. If you want
more extensive information, you can use available `attachments` to get more
information in the results (like subscribers and projects).
Generally, requesting more information means the query executes more slowly
and returns more data (in some cases, much more data). You should normally
request only the data you need.
To request extra data, specify which attachments you want in the `attachments`
parameter:
```lang=json, name="Example Attachments Request"
{
...
"attachments": {
"subscribers": true
},
...
}
```
This example specifies that results should include information about
subscribers. In the return value, each object will now have this information
filled out in the corresponding `attachments` value:
```lang=json, name="Example Attachments Result"
{
...
"data": [
{
...
"attachments": {
"subscribers": {
"subscriberPHIDs": [
"PHID-WXYZ-2222",
],
"subscriberCount": 1,
"viewerIsSubscribed": false
}
},
...
},
...
],
...
}
```
These attachments are available:
EOTEXT
);
$attachments = $engine->getConduitSearchAttachments();
$rows = array();
foreach ($attachments as $key => $attachment) {
$rows[] = array(
$key,
$attachment->getAttachmentName(),
$attachment->getAttachmentDescription(),
);
}
$table = id(new AphrontTableView($rows))
->setNoDataString(pht('This call does not support any attachments.'))
->setHeaders(
array(
pht('Key'),
pht('Name'),
pht('Description'),
))
->setColumnClasses(
array(
'prewrap',
'pri',
'wide',
));
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Attachments'))
->setCollapsed(true)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
- ->appendChild($this->buildRemarkup($info))
+ ->appendChild($this->newRemarkupDocumentationView($info))
->appendChild($table);
}
private function buildPagingBox(
PhabricatorApplicationSearchEngine $engine) {
$info = pht(<<<EOTEXT
Queries are limited to returning 100 results at a time. If you want fewer
results than this, you can use `limit` to specify a smaller limit.
If you want more results, you'll need to make additional queries to retrieve
more pages of results.
The result structure contains a `cursor` key with information you'll need in
order to fetch the next page of results. After an initial query, it will
usually look something like this:
```lang=json, name="Example Cursor Result"
{
...
"cursor": {
"limit": 100,
"after": "1234",
"before": null,
"order": null
}
...
}
```
The `limit` and `order` fields are describing the effective limit and order the
query was executed with, and are usually not of much interest. The `after` and
`before` fields give you cursors which you can pass when making another API
call in order to get the next (or previous) page of results.
To get the next page of results, repeat your API call with all the same
parameters as the original call, but pass the `after` cursor you received from
the first call in the `after` parameter when making the second call.
If you do things correctly, you should get the second page of results, and
a cursor structure like this:
```lang=json, name="Second Result Page"
{
...
"cursor": {
"limit": 5,
"after": "4567",
"before": "7890",
"order": null
}
...
}
```
You can now continue to the third page of results by passing the new `after`
cursor to the `after` parameter in your third call, or return to the previous
page of results by passing the `before` cursor to the `before` parameter. This
might be useful if you are rendering a web UI for a user and want to provide
"Next Page" and "Previous Page" links.
If `after` is `null`, there is no next page of results available. Likewise,
if `before` is `null`, there are no previous results available.
EOTEXT
);
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Paging and Limits'))
->setCollapsed(true)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
- ->appendChild($this->buildRemarkup($info));
+ ->appendChild($this->newRemarkupDocumentationView($info));
}
- private function buildRemarkup($remarkup) {
- $viewer = $this->getViewer();
-
- $view = new PHUIRemarkupView($viewer, $remarkup);
-
- $view->setRemarkupOptions(
- array(
- PHUIRemarkupView::OPTION_PRESERVE_LINEBREAKS => false,
- ));
-
- return id(new PHUIBoxView())
- ->appendChild($view)
- ->addPadding(PHUI::PADDING_LARGE);
- }
}
diff --git a/src/applications/search/engineextension/PhabricatorFulltextIndexEngineExtension.php b/src/applications/search/engineextension/PhabricatorFulltextIndexEngineExtension.php
index ab4da8842..126f298f1 100644
--- a/src/applications/search/engineextension/PhabricatorFulltextIndexEngineExtension.php
+++ b/src/applications/search/engineextension/PhabricatorFulltextIndexEngineExtension.php
@@ -1,116 +1,112 @@
<?php
final class PhabricatorFulltextIndexEngineExtension
extends PhabricatorIndexEngineExtension {
const EXTENSIONKEY = 'fulltext';
private $configurationVersion;
public function getExtensionName() {
return pht('Fulltext Engine');
}
public function getIndexVersion($object) {
$version = array();
// When "cluster.search" is reconfigured, new indexes which don't have any
// data yet may have been added. We err on the side of caution and assume
// that every document may need to be reindexed.
$version[] = $this->getConfigurationVersion();
if ($object instanceof PhabricatorApplicationTransactionInterface) {
// If this is a normal object with transactions, we only need to
// reindex it if there are new transactions (or comment edits).
$version[] = $this->getTransactionVersion($object);
$version[] = $this->getCommentVersion($object);
}
if (!$version) {
return null;
}
return implode(':', $version);
}
public function shouldIndexObject($object) {
return ($object instanceof PhabricatorFulltextInterface);
}
public function indexObject(
PhabricatorIndexEngine $engine,
$object) {
$engine = $object->newFulltextEngine();
if (!$engine) {
return;
}
$engine->setObject($object);
$engine->buildFulltextIndexes();
}
private function getTransactionVersion($object) {
$xaction = $object->getApplicationTransactionTemplate();
$xaction_row = queryfx_one(
$xaction->establishConnection('r'),
'SELECT id FROM %T WHERE objectPHID = %s
ORDER BY id DESC LIMIT 1',
$xaction->getTableName(),
$object->getPHID());
if (!$xaction_row) {
return 'none';
}
return $xaction_row['id'];
}
private function getCommentVersion($object) {
$xaction = $object->getApplicationTransactionTemplate();
- try {
- $comment = $xaction->getApplicationTransactionCommentObject();
- if (!$comment) {
- return 'none';
- }
- } catch (Exception $ex) {
+ $comment = $xaction->getApplicationTransactionCommentObject();
+ if (!$comment) {
return 'none';
}
$comment_row = queryfx_one(
$comment->establishConnection('r'),
'SELECT c.id FROM %T x JOIN %T c
ON x.phid = c.transactionPHID
WHERE x.objectPHID = %s
ORDER BY c.id DESC LIMIT 1',
$xaction->getTableName(),
$comment->getTableName(),
$object->getPHID());
if (!$comment_row) {
return 'none';
}
return $comment_row['id'];
}
private function getConfigurationVersion() {
if ($this->configurationVersion === null) {
$this->configurationVersion = $this->newConfigurationVersion();
}
return $this->configurationVersion;
}
private function newConfigurationVersion() {
$raw = array(
'services' => PhabricatorEnv::getEnvConfig('cluster.search'),
);
$json = phutil_json_encode($raw);
return PhabricatorHash::digestForIndex($json);
}
}
diff --git a/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php b/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php
index 6b6d25cb6..984eeae5f 100644
--- a/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php
+++ b/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php
@@ -1,284 +1,297 @@
<?php
final class PhabricatorSearchManagementIndexWorkflow
extends PhabricatorSearchManagementWorkflow {
protected function didConstruct() {
$this
->setName('index')
->setSynopsis(pht('Build or rebuild search indexes.'))
->setExamples(
"**index** D123\n".
"**index** --type task\n".
"**index** --all")
->setArguments(
array(
array(
'name' => 'all',
'help' => pht('Reindex all documents.'),
),
array(
'name' => 'type',
'param' => 'type',
'help' => pht(
'Object types to reindex, like "task", "commit" or "revision".'),
),
array(
'name' => 'background',
'help' => pht(
'Instead of indexing in this process, queue tasks for '.
'the daemons. This can improve performance, but makes '.
'it more difficult to debug search indexing.'),
),
array(
'name' => 'force',
'short' => 'f',
'help' => pht(
'Force a complete rebuild of the entire index instead of an '.
'incremental update.'),
),
array(
'name' => 'objects',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$this->validateClusterSearchConfig();
$console = PhutilConsole::getConsole();
$is_all = $args->getArg('all');
$is_type = $args->getArg('type');
$is_force = $args->getArg('force');
$obj_names = $args->getArg('objects');
if ($obj_names && ($is_all || $is_type)) {
throw new PhutilArgumentUsageException(
pht(
"You can not name objects to index alongside the '%s' or '%s' flags.",
'--all',
'--type'));
} else if (!$obj_names && !($is_all || $is_type)) {
throw new PhutilArgumentUsageException(
pht(
"Provide one of '%s', '%s' or a list of object names.",
'--all',
'--type'));
}
if ($obj_names) {
$phids = $this->loadPHIDsByNames($obj_names);
} else {
$phids = $this->loadPHIDsByTypes($is_type);
}
if (!$phids) {
throw new PhutilArgumentUsageException(pht('Nothing to index!'));
}
if ($args->getArg('background')) {
$is_background = true;
} else {
PhabricatorWorker::setRunAllTasksInProcess(true);
$is_background = false;
}
if (!$is_background) {
echo tsprintf(
"**<bg:blue> %s </bg>** %s\n",
pht('NOTE'),
pht(
'Run this workflow with "%s" to queue tasks for the daemon workers.',
'--background'));
}
$groups = phid_group_by_type($phids);
foreach ($groups as $group_type => $group) {
$console->writeOut(
"%s\n",
pht('Indexing %d object(s) of type %s.', count($group), $group_type));
}
$bar = id(new PhutilConsoleProgressBar())
->setTotal(count($phids));
$parameters = array(
'force' => $is_force,
);
$any_success = false;
// If we aren't using "--background" or "--force", track how many objects
// we're skipping so we can print this information for the user and give
// them a hint that they might want to use "--force".
$track_skips = (!$is_background && !$is_force);
+ // Activate "strict" error reporting if we're running in the foreground
+ // so we'll report a wider range of conditions as errors.
+ $is_strict = !$is_background;
+
$count_updated = 0;
$count_skipped = 0;
foreach ($phids as $phid) {
try {
if ($track_skips) {
$old_versions = $this->loadIndexVersions($phid);
}
- PhabricatorSearchWorker::queueDocumentForIndexing($phid, $parameters);
+ PhabricatorSearchWorker::queueDocumentForIndexing(
+ $phid,
+ $parameters,
+ $is_strict);
if ($track_skips) {
$new_versions = $this->loadIndexVersions($phid);
- if ($old_versions !== $new_versions) {
+
+ if (!$old_versions && !$new_versions) {
+ // If the document doesn't use an index version, both the lists
+ // of versions will be empty. We still rebuild the index in this
+ // case.
+ $count_updated++;
+ } else if ($old_versions !== $new_versions) {
$count_updated++;
} else {
$count_skipped++;
}
}
$any_success = true;
} catch (Exception $ex) {
phlog($ex);
}
$bar->update(1);
}
$bar->done();
if (!$any_success) {
throw new Exception(
pht('Failed to rebuild search index for any documents.'));
}
if ($track_skips) {
if ($count_updated) {
echo tsprintf(
"**<bg:green> %s </bg>** %s\n",
pht('DONE'),
pht(
'Updated search indexes for %s document(s).',
new PhutilNumber($count_updated)));
}
if ($count_skipped) {
echo tsprintf(
"**<bg:yellow> %s </bg>** %s\n",
pht('SKIP'),
pht(
'Skipped %s documents(s) which have not updated since they were '.
'last indexed.',
new PhutilNumber($count_skipped)));
echo tsprintf(
"**<bg:blue> %s </bg>** %s\n",
pht('NOTE'),
pht(
'Use "--force" to force the index to update these documents.'));
}
} else if ($is_background) {
echo tsprintf(
"**<bg:green> %s </bg>** %s\n",
pht('DONE'),
pht(
'Queued %s document(s) for background indexing.',
new PhutilNumber(count($phids))));
} else {
echo tsprintf(
"**<bg:green> %s </bg>** %s\n",
pht('DONE'),
pht(
'Forced search index updates for %s document(s).',
new PhutilNumber(count($phids))));
}
}
private function loadPHIDsByNames(array $names) {
$query = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->withNames($names);
$query->execute();
$objects = $query->getNamedResults();
foreach ($names as $name) {
if (empty($objects[$name])) {
throw new PhutilArgumentUsageException(
pht(
"'%s' is not the name of a known object.",
$name));
}
}
return mpull($objects, 'getPHID');
}
private function loadPHIDsByTypes($type) {
$objects = id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorIndexableInterface')
->execute();
$normalized_type = phutil_utf8_strtolower($type);
$matches = array();
foreach ($objects as $object) {
$object_class = get_class($object);
$normalized_class = phutil_utf8_strtolower($object_class);
if ($normalized_class === $normalized_type) {
$matches = array($object_class => $object);
break;
}
if (!strlen($type) ||
strpos($normalized_class, $normalized_type) !== false) {
$matches[$object_class] = $object;
}
}
if (!$matches) {
$all_types = array();
foreach ($objects as $object) {
$all_types[] = get_class($object);
}
sort($all_types);
throw new PhutilArgumentUsageException(
pht(
'Type "%s" matches no indexable objects. Supported types are: %s.',
$type,
implode(', ', $all_types)));
}
if ((count($matches) > 1) && strlen($type)) {
throw new PhutilArgumentUsageException(
pht(
'Type "%s" matches multiple indexable objects. Use a more '.
'specific string. Matching object types are: %s.',
$type,
implode(', ', array_keys($matches))));
}
$phids = array();
foreach ($matches as $match) {
$iterator = new LiskMigrationIterator($match);
foreach ($iterator as $object) {
$phids[] = $object->getPHID();
}
}
return $phids;
}
private function loadIndexVersions($phid) {
$table = new PhabricatorSearchIndexVersion();
$conn = $table->establishConnection('r');
return queryfx_all(
$conn,
'SELECT extensionKey, version FROM %T WHERE objectPHID = %s
ORDER BY extensionKey, version',
$table->getTableName(),
$phid);
}
}
diff --git a/src/applications/search/menuitem/PhabricatorConpherenceProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorConpherenceProfileMenuItem.php
index 6a91188c8..542c63495 100644
--- a/src/applications/search/menuitem/PhabricatorConpherenceProfileMenuItem.php
+++ b/src/applications/search/menuitem/PhabricatorConpherenceProfileMenuItem.php
@@ -1,177 +1,183 @@
<?php
final class PhabricatorConpherenceProfileMenuItem
extends PhabricatorProfileMenuItem {
const MENUITEMKEY = 'conpherence';
const FIELD_CONPHERENCE = 'conpherence';
private $conpherence;
public function getMenuItemTypeIcon() {
return 'fa-comments';
}
public function getMenuItemTypeName() {
return pht('Conpherence');
}
public function canAddToObject($object) {
+ $application = new PhabricatorConpherenceApplication();
+
+ if (!$application->isInstalled()) {
+ return false;
+ }
+
return true;
}
public function attachConpherence($conpherence) {
$this->conpherence = $conpherence;
return $this;
}
public function getConpherence() {
$conpherence = $this->conpherence;
if (!$conpherence) {
return null;
}
return $conpherence;
}
public function willBuildNavigationItems(array $items) {
$viewer = $this->getViewer();
$room_phids = array();
foreach ($items as $item) {
$room_phids[] = $item->getMenuItemProperty('conpherence');
}
$rooms = id(new ConpherenceThreadQuery())
->setViewer($viewer)
->withPHIDs($room_phids)
->needProfileImage(true)
->execute();
$rooms = mpull($rooms, null, 'getPHID');
foreach ($items as $item) {
$room_phid = $item->getMenuItemProperty('conpherence');
$room = idx($rooms, $room_phid, null);
$item->getMenuItem()->attachConpherence($room);
}
}
public function getDisplayName(
PhabricatorProfileMenuItemConfiguration $config) {
$room = $this->getConpherence($config);
if (!$room) {
return pht('(Restricted/Invalid Conpherence)');
}
$name = $this->getName($config);
if (strlen($name)) {
return $name;
}
return $room->getTitle();
}
public function buildEditEngineFields(
PhabricatorProfileMenuItemConfiguration $config) {
return array(
id(new PhabricatorDatasourceEditField())
->setKey(self::FIELD_CONPHERENCE)
->setLabel(pht('Conpherence Room'))
->setDatasource(new ConpherenceThreadDatasource())
->setIsRequired(true)
->setSingleValue($config->getMenuItemProperty('conpherence')),
id(new PhabricatorTextEditField())
->setKey('name')
->setLabel(pht('Name'))
->setValue($this->getName($config)),
);
}
private function getName(
PhabricatorProfileMenuItemConfiguration $config) {
return $config->getMenuItemProperty('name');
}
protected function newNavigationMenuItems(
PhabricatorProfileMenuItemConfiguration $config) {
$viewer = $this->getViewer();
$room = $this->getConpherence($config);
if (!$room) {
return array();
}
$participants = $room->getParticipants();
$viewer_phid = $viewer->getPHID();
$unread_count = null;
if (isset($participants[$viewer_phid])) {
$data = $room->getDisplayData($viewer);
$unread_count = $data['unread_count'];
}
$count = null;
if ($unread_count) {
$count = phutil_tag(
'span',
array(
'class' => 'phui-list-item-count',
),
$unread_count);
}
$item = $this->newItem()
->setHref('/'.$room->getMonogram())
->setName($this->getDisplayName($config))
->setIcon('fa-comments')
->appendChild($count);
return array(
$item,
);
}
public function validateTransactions(
PhabricatorProfileMenuItemConfiguration $config,
$field_key,
$value,
array $xactions) {
$viewer = $this->getViewer();
$errors = array();
if ($field_key == self::FIELD_CONPHERENCE) {
if ($this->isEmptyTransaction($value, $xactions)) {
$errors[] = $this->newRequiredError(
pht('You must choose a room.'),
$field_key);
}
foreach ($xactions as $xaction) {
$new = $xaction['new'];
if (!$new) {
continue;
}
if ($new === $value) {
continue;
}
$rooms = id(new ConpherenceThreadQuery())
->setViewer($viewer)
->withPHIDs(array($new))
->execute();
if (!$rooms) {
$errors[] = $this->newInvalidError(
pht(
'Room "%s" is not a valid room which you have '.
'permission to see.',
$new),
$xaction['xaction']);
}
}
}
return $errors;
}
}
diff --git a/src/applications/search/storage/PhabricatorProfileMenuItemConfigurationTransaction.php b/src/applications/search/storage/PhabricatorProfileMenuItemConfigurationTransaction.php
index b1d30a5b9..4624d6f9a 100644
--- a/src/applications/search/storage/PhabricatorProfileMenuItemConfigurationTransaction.php
+++ b/src/applications/search/storage/PhabricatorProfileMenuItemConfigurationTransaction.php
@@ -1,27 +1,23 @@
<?php
final class PhabricatorProfileMenuItemConfigurationTransaction
extends PhabricatorApplicationTransaction {
const TYPE_PROPERTY = 'profilepanel.property';
const TYPE_ORDER = 'profilepanel.order';
const TYPE_VISIBILITY = 'profilepanel.visibility';
public function getApplicationName() {
return 'search';
}
public function getTableName() {
// At least for now, this object uses an older table name.
return 'search_profilepanelconfigurationtransaction';
}
public function getApplicationTransactionType() {
return PhabricatorProfileMenuItemPHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return null;
- }
-
}
diff --git a/src/applications/search/worker/PhabricatorSearchWorker.php b/src/applications/search/worker/PhabricatorSearchWorker.php
index f93df6398..31c68d45c 100644
--- a/src/applications/search/worker/PhabricatorSearchWorker.php
+++ b/src/applications/search/worker/PhabricatorSearchWorker.php
@@ -1,94 +1,125 @@
<?php
final class PhabricatorSearchWorker extends PhabricatorWorker {
- public static function queueDocumentForIndexing($phid, $parameters = null) {
+ public static function queueDocumentForIndexing(
+ $phid,
+ $parameters = null,
+ $is_strict = false) {
+
if ($parameters === null) {
$parameters = array();
}
parent::scheduleTask(
__CLASS__,
array(
'documentPHID' => $phid,
'parameters' => $parameters,
+ 'strict' => $is_strict,
),
array(
- 'priority' => parent::PRIORITY_IMPORT,
+ 'priority' => parent::PRIORITY_INDEX,
'objectPHID' => $phid,
));
}
protected function doWork() {
$data = $this->getTaskData();
$object_phid = idx($data, 'documentPHID');
- $object = $this->loadObjectForIndexing($object_phid);
+ // See T12425. By the time we run an indexing task, the object it indexes
+ // may have been deleted. This is unusual, but not concerning, and failing
+ // to index these objects is correct.
+
+ // To avoid showing these non-actionable errors to users, don't report
+ // indexing exceptions unless we're in "strict" mode. This mode is set by
+ // the "bin/search index" tool.
+
+ $is_strict = idx($data, 'strict', false);
+
+ try {
+ $object = $this->loadObjectForIndexing($object_phid);
+ } catch (PhabricatorWorkerPermanentFailureException $ex) {
+ if ($is_strict) {
+ throw $ex;
+ } else {
+ return;
+ }
+ }
$engine = id(new PhabricatorIndexEngine())
->setObject($object);
$parameters = idx($data, 'parameters', array());
$engine->setParameters($parameters);
if (!$engine->shouldIndexObject()) {
return;
}
- $key = "index.{$object_phid}";
- $lock = PhabricatorGlobalLock::newLock($key);
+ $lock = PhabricatorGlobalLock::newLock(
+ 'index',
+ array(
+ 'objectPHID' => $object_phid,
+ ));
try {
$lock->lock(1);
} catch (PhutilLockException $ex) {
// If we fail to acquire the lock, just yield. It's expected that we may
// contend on this lock occasionally if a large object receives many
// updates in a short period of time, and it's appropriate to just retry
// rebuilding the index later.
throw new PhabricatorWorkerYieldException(15);
}
+ $caught = null;
try {
// Reload the object now that we have a lock, to make sure we have the
// most current version.
$object = $this->loadObjectForIndexing($object->getPHID());
$engine->setObject($object);
-
$engine->indexObject();
} catch (Exception $ex) {
- $lock->unlock();
+ $caught = $ex;
+ }
+
+ // Release the lock before we deal with the exception.
+ $lock->unlock();
- if (!($ex instanceof PhabricatorWorkerPermanentFailureException)) {
- $ex = new PhabricatorWorkerPermanentFailureException(
+ if ($caught) {
+ if (!($caught instanceof PhabricatorWorkerPermanentFailureException)) {
+ $caught = new PhabricatorWorkerPermanentFailureException(
pht(
'Failed to update search index for document "%s": %s',
$object_phid,
- $ex->getMessage()));
+ $caught->getMessage()));
}
- throw $ex;
+ if ($is_strict) {
+ throw $caught;
+ }
}
-
- $lock->unlock();
}
private function loadObjectForIndexing($phid) {
$viewer = PhabricatorUser::getOmnipotentUser();
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
if (!$object) {
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Unable to load object "%s" to rebuild indexes.',
$phid));
}
return $object;
}
}
diff --git a/src/applications/settings/controller/PhabricatorSettingsTimezoneController.php b/src/applications/settings/controller/PhabricatorSettingsTimezoneController.php
index 51f1747b9..6a0ba19d0 100644
--- a/src/applications/settings/controller/PhabricatorSettingsTimezoneController.php
+++ b/src/applications/settings/controller/PhabricatorSettingsTimezoneController.php
@@ -1,146 +1,151 @@
<?php
final class PhabricatorSettingsTimezoneController
extends PhabricatorController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$client_offset = $request->getURIData('offset');
$client_offset = (int)$client_offset;
$timezones = DateTimeZone::listIdentifiers();
$now = new DateTime('@'.PhabricatorTime::getNow());
$options = array(
'ignore' => pht('Ignore Conflict'),
);
foreach ($timezones as $identifier) {
$zone = new DateTimeZone($identifier);
$offset = -($zone->getOffset($now) / 60);
if ($offset == $client_offset) {
$options[$identifier] = $identifier;
}
}
$settings_help = pht(
'You can change your date and time preferences in Settings.');
$did_calibrate = false;
if ($request->isFormPost()) {
$timezone = $request->getStr('timezone');
$pref_ignore = PhabricatorTimezoneIgnoreOffsetSetting::SETTINGKEY;
$pref_timezone = PhabricatorTimezoneSetting::SETTINGKEY;
if ($timezone == 'ignore') {
$this->writeSettings(
array(
$pref_ignore => $client_offset,
));
return $this->newDialog()
->setTitle(pht('Conflict Ignored'))
->appendParagraph(
pht(
'The conflict between your browser and profile timezone '.
'settings will be ignored.'))
->appendParagraph($settings_help)
->addCancelButton('/', pht('Done'));
}
if (isset($options[$timezone])) {
$this->writeSettings(
array(
$pref_ignore => null,
$pref_timezone => $timezone,
));
$did_calibrate = true;
}
}
$server_offset = $viewer->getTimeZoneOffset();
if (($client_offset == $server_offset) || $did_calibrate) {
return $this->newDialog()
->setTitle(pht('Timezone Calibrated'))
->appendParagraph(
pht(
'Your browser timezone and profile timezone are now '.
'in agreement (%s).',
$this->formatOffset($client_offset)))
->appendParagraph($settings_help)
->addCancelButton('/', pht('Done'));
}
// If we have a guess at the timezone from the client, select it as the
// default.
$guess = $request->getStr('guess');
if (empty($options[$guess])) {
$guess = 'ignore';
}
$current_zone = $viewer->getTimezoneIdentifier();
$current_zone = phutil_tag('strong', array(), $current_zone);
$form = id(new AphrontFormView())
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Current Setting'))
->setValue($current_zone))
->appendChild(
id(new AphrontFormSelectControl())
->setName('timezone')
->setLabel(pht('New Setting'))
->setOptions($options)
->setValue($guess));
return $this->newDialog()
->setTitle(pht('Adjust Timezone'))
->setWidth(AphrontDialogView::WIDTH_FORM)
->appendParagraph(
pht(
'Your browser timezone (%s) differs from your profile timezone '.
'(%s). You can ignore this conflict or adjust your profile setting '.
'to match your client.',
$this->formatOffset($client_offset),
$this->formatOffset($server_offset)))
->appendForm($form)
->addCancelButton(pht('Cancel'))
->addSubmitButton(pht('Change Timezone'));
}
private function formatOffset($offset) {
+ // This controller works with client-side (Javascript) offsets, which have
+ // the opposite sign we might expect -- for example "UTC-3" is a positive
+ // offset. Invert the sign before rendering the offset.
+ $offset = -1 * $offset;
+
$hours = $offset / 60;
// Non-integer number of hours off UTC?
if ($offset % 60) {
$minutes = abs($offset % 60);
return pht('UTC%+d:%02d', $hours, $minutes);
} else {
return pht('UTC%+d', $hours);
}
}
private function writeSettings(array $map) {
$request = $this->getRequest();
$viewer = $this->getViewer();
$preferences = PhabricatorUserPreferences::loadUserPreferences($viewer);
$editor = id(new PhabricatorUserPreferencesEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$xactions = array();
foreach ($map as $key => $value) {
$xactions[] = $preferences->newTransaction($key, $value);
}
$editor->applyTransactions($preferences, $xactions);
}
}
diff --git a/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php b/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php
index 1b69adcd6..8f7f633e7 100644
--- a/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php
@@ -1,415 +1,414 @@
<?php
final class PhabricatorEmailAddressesSettingsPanel
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'email';
}
public function getPanelName() {
return pht('Email Addresses');
}
public function getPanelMenuIcon() {
return 'fa-at';
}
public function getPanelGroupKey() {
return PhabricatorSettingsEmailPanelGroup::PANELGROUPKEY;
}
public function isEditableByAdministrators() {
if ($this->getUser()->getIsMailingList()) {
return true;
}
return false;
}
public function processRequest(AphrontRequest $request) {
$user = $this->getUser();
$editable = PhabricatorEnv::getEnvConfig('account.editable');
- $uri = $request->getRequestURI();
- $uri->setQueryParams(array());
+ $uri = new PhutilURI($request->getPath());
if ($editable) {
$new = $request->getStr('new');
if ($new) {
return $this->returnNewAddressResponse($request, $uri, $new);
}
$delete = $request->getInt('delete');
if ($delete) {
return $this->returnDeleteAddressResponse($request, $uri, $delete);
}
}
$verify = $request->getInt('verify');
if ($verify) {
return $this->returnVerifyAddressResponse($request, $uri, $verify);
}
$primary = $request->getInt('primary');
if ($primary) {
return $this->returnPrimaryAddressResponse($request, $uri, $primary);
}
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
'userPHID = %s ORDER BY address',
$user->getPHID());
$rowc = array();
$rows = array();
foreach ($emails as $email) {
$button_verify = javelin_tag(
'a',
array(
'class' => 'button small button-grey',
'href' => $uri->alter('verify', $email->getID()),
'sigil' => 'workflow',
),
pht('Verify'));
$button_make_primary = javelin_tag(
'a',
array(
'class' => 'button small button-grey',
'href' => $uri->alter('primary', $email->getID()),
'sigil' => 'workflow',
),
pht('Make Primary'));
$button_remove = javelin_tag(
'a',
array(
'class' => 'button small button-grey',
'href' => $uri->alter('delete', $email->getID()),
'sigil' => 'workflow',
),
pht('Remove'));
$button_primary = phutil_tag(
'a',
array(
'class' => 'button small disabled',
),
pht('Primary'));
if (!$email->getIsVerified()) {
$action = $button_verify;
} else if ($email->getIsPrimary()) {
$action = $button_primary;
} else {
$action = $button_make_primary;
}
if ($email->getIsPrimary()) {
$remove = $button_primary;
$rowc[] = 'highlighted';
} else {
$remove = $button_remove;
$rowc[] = null;
}
$rows[] = array(
$email->getAddress(),
$action,
$remove,
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
pht('Email'),
pht('Status'),
pht('Remove'),
));
$table->setColumnClasses(
array(
'wide',
'action',
'action',
));
$table->setRowClasses($rowc);
$table->setColumnVisibility(
array(
true,
true,
$editable,
));
$buttons = array();
if ($editable) {
$buttons[] = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-plus')
->setText(pht('Add New Address'))
->setHref($uri->alter('new', 'true'))
->addSigil('workflow')
->setColor(PHUIButtonView::GREY);
}
return $this->newBox(pht('Email Addresses'), $table, $buttons);
}
private function returnNewAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$new) {
$user = $this->getUser();
$viewer = $this->getViewer();
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$this->getPanelURI());
$e_email = true;
$email = null;
$errors = array();
if ($request->isDialogFormPost()) {
$email = trim($request->getStr('email'));
if ($new == 'verify') {
// The user clicked "Done" from the "an email has been sent" dialog.
return id(new AphrontReloadResponse())->setURI($uri);
}
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorSettingsAddEmailAction(),
1);
if (!strlen($email)) {
$e_email = pht('Required');
$errors[] = pht('Email is required.');
} else if (!PhabricatorUserEmail::isValidAddress($email)) {
$e_email = pht('Invalid');
$errors[] = PhabricatorUserEmail::describeValidAddresses();
} else if (!PhabricatorUserEmail::isAllowedAddress($email)) {
$e_email = pht('Disallowed');
$errors[] = PhabricatorUserEmail::describeAllowedAddresses();
}
if ($e_email === true) {
$application_email = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withAddresses(array($email))
->executeOne();
if ($application_email) {
$e_email = pht('In Use');
$errors[] = $application_email->getInUseMessage();
}
}
if (!$errors) {
$object = id(new PhabricatorUserEmail())
->setAddress($email)
->setIsVerified(0);
// If an administrator is editing a mailing list, automatically verify
// the address.
if ($viewer->getPHID() != $user->getPHID()) {
if ($viewer->getIsAdmin()) {
$object->setIsVerified(1);
}
}
try {
id(new PhabricatorUserEditor())
->setActor($viewer)
->addEmail($user, $object);
if ($object->getIsVerified()) {
// If we autoverified the address, just reload the page.
return id(new AphrontReloadResponse())->setURI($uri);
}
$object->sendVerificationEmail($user);
$dialog = $this->newDialog()
->addHiddenInput('new', 'verify')
->setTitle(pht('Verification Email Sent'))
->appendChild(phutil_tag('p', array(), pht(
'A verification email has been sent. Click the link in the '.
'email to verify your address.')))
->setSubmitURI($uri)
->addSubmitButton(pht('Done'));
return id(new AphrontDialogResponse())->setDialog($dialog);
} catch (AphrontDuplicateKeyQueryException $ex) {
$e_email = pht('Duplicate');
$errors[] = pht('Another user already has this email.');
}
}
}
if ($errors) {
$errors = id(new PHUIInfoView())
->setErrors($errors);
}
$form = id(new PHUIFormLayoutView())
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Email'))
->setName('email')
->setValue($email)
->setCaption(PhabricatorUserEmail::describeAllowedAddresses())
->setError($e_email));
$dialog = $this->newDialog()
->addHiddenInput('new', 'true')
->setTitle(pht('New Address'))
->appendChild($errors)
->appendChild($form)
->addSubmitButton(pht('Save'))
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function returnDeleteAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_id) {
$user = $this->getUser();
$viewer = $this->getViewer();
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$this->getPanelURI());
// NOTE: You can only delete your own email addresses, and you can not
// delete your primary address.
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'id = %d AND userPHID = %s AND isPrimary = 0',
$email_id,
$user->getPHID());
if (!$email) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
id(new PhabricatorUserEditor())
->setActor($viewer)
->removeEmail($user, $email);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$address = $email->getAddress();
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addHiddenInput('delete', $email_id)
->setTitle(pht("Really delete address '%s'?", $address))
->appendParagraph(
pht(
'Are you sure you want to delete this address? You will no '.
'longer be able to use it to login.'))
->appendParagraph(
pht(
'Note: Removing an email address from your account will invalidate '.
'any outstanding password reset links.'))
->addSubmitButton(pht('Delete'))
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function returnVerifyAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_id) {
$user = $this->getUser();
$viewer = $this->getViewer();
// NOTE: You can only send more email for your unverified addresses.
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'id = %d AND userPHID = %s AND isVerified = 0',
$email_id,
$user->getPHID());
if (!$email) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
$email->sendVerificationEmail($user);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$address = $email->getAddress();
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addHiddenInput('verify', $email_id)
->setTitle(pht('Send Another Verification Email?'))
->appendChild(phutil_tag('p', array(), pht(
'Send another copy of the verification email to %s?',
$address)))
->addSubmitButton(pht('Send Email'))
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function returnPrimaryAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_id) {
$user = $this->getUser();
$viewer = $this->getViewer();
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$this->getPanelURI());
// NOTE: You can only make your own verified addresses primary.
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'id = %d AND userPHID = %s AND isVerified = 1 AND isPrimary = 0',
$email_id,
$user->getPHID());
if (!$email) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
id(new PhabricatorUserEditor())
->setActor($viewer)
->changePrimaryEmail($user, $email);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$address = $email->getAddress();
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addHiddenInput('primary', $email_id)
->setTitle(pht('Change primary email address?'))
->appendParagraph(
pht(
'If you change your primary address, Phabricator will send all '.
'email to %s.',
$address))
->appendParagraph(
pht(
'Note: Changing your primary email address will invalidate any '.
'outstanding password reset links.'))
->addSubmitButton(pht('Change Primary Address'))
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php b/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php
index 121548720..60389e159 100644
--- a/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php
@@ -1,141 +1,141 @@
<?php
final class PhabricatorExternalAccountsSettingsPanel
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'external';
}
public function getPanelName() {
return pht('External Accounts');
}
public function getPanelMenuIcon() {
return 'fa-users';
}
public function getPanelGroupKey() {
return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY;
}
public function processRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$providers = PhabricatorAuthProvider::getAllProviders();
$accounts = id(new PhabricatorExternalAccountQuery())
->setViewer($viewer)
->withUserPHIDs(array($viewer->getPHID()))
->needImages(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->execute();
$linked_head = pht('Linked Accounts and Authentication');
$linked = id(new PHUIObjectItemListView())
->setUser($viewer)
->setNoDataString(pht('You have no linked accounts.'));
- $login_accounts = 0;
- foreach ($accounts as $account) {
- if ($account->isUsableForLogin()) {
- $login_accounts++;
- }
- }
-
foreach ($accounts as $account) {
$item = new PHUIObjectItemView();
- $provider = idx($providers, $account->getProviderKey());
- if ($provider) {
- $item->setHeader($provider->getProviderName());
- $can_unlink = $provider->shouldAllowAccountUnlink();
- if (!$can_unlink) {
- $item->addAttribute(pht('Permanently Linked'));
- }
- } else {
- $item->setHeader(
- pht('Unknown Account ("%s")', $account->getProviderKey()));
- $can_unlink = true;
+ $config = $account->getProviderConfig();
+ $provider = $config->getProvider();
+
+ $item->setHeader($provider->getProviderName());
+ $can_unlink = $provider->shouldAllowAccountUnlink();
+ if (!$can_unlink) {
+ $item->addAttribute(pht('Permanently Linked'));
}
$can_login = $account->isUsableForLogin();
if (!$can_login) {
$item->addAttribute(
pht(
'Disabled (an administrator has disabled login for this '.
'account provider).'));
}
- $can_unlink = $can_unlink && (!$can_login || ($login_accounts > 1));
-
- $can_refresh = $provider && $provider->shouldAllowAccountRefresh();
+ $can_refresh = $provider->shouldAllowAccountRefresh();
if ($can_refresh) {
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-refresh')
- ->setHref('/auth/refresh/'.$account->getProviderKey().'/'));
+ ->setHref('/auth/refresh/'.$config->getID().'/'));
}
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-times')
->setWorkflow(true)
->setDisabled(!$can_unlink)
- ->setHref('/auth/unlink/'.$account->getProviderKey().'/'));
+ ->setHref('/auth/unlink/'.$account->getID().'/'));
if ($provider) {
$provider->willRenderLinkedAccount($viewer, $item, $account);
}
$linked->addItem($item);
}
$linkable_head = pht('Add External Account');
$linkable = id(new PHUIObjectItemListView())
->setUser($viewer)
->setNoDataString(
pht('Your account is linked with all available providers.'));
- $accounts = mpull($accounts, null, 'getProviderKey');
+ $configs = id(new PhabricatorAuthProviderConfigQuery())
+ ->setViewer($viewer)
+ ->withIsEnabled(true)
+ ->execute();
+ $configs = msort($configs, 'getSortVector');
- $providers = PhabricatorAuthProvider::getAllEnabledProviders();
- $providers = msort($providers, 'getProviderName');
- foreach ($providers as $key => $provider) {
- if (isset($accounts[$key])) {
+ $account_map = mgroup($accounts, 'getProviderConfigPHID');
+
+
+ foreach ($configs as $config) {
+ $provider = $config->getProvider();
+
+ if (!$provider->shouldAllowAccountLink()) {
continue;
}
- if (!$provider->shouldAllowAccountLink()) {
+ // Don't show the user providers they already have linked.
+ if (isset($account_map[$config->getPHID()])) {
continue;
}
- $link_uri = '/auth/link/'.$provider->getProviderKey().'/';
+ $link_uri = '/auth/link/'.$config->getID().'/';
+
+ $link_button = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setIcon('fa-link')
+ ->setHref($link_uri)
+ ->setColor(PHUIButtonView::GREY)
+ ->setText(pht('Link External Account'));
$item = id(new PHUIObjectItemView())
- ->setHeader($provider->getProviderName())
+ ->setHeader($config->getDisplayName())
->setHref($link_uri)
- ->addAction(
- id(new PHUIListItemView())
- ->setIcon('fa-link')
- ->setHref($link_uri));
+ ->setImageIcon($config->newIconView())
+ ->setSideColumn($link_button);
$linkable->addItem($item);
}
$linked_box = $this->newBox($linked_head, $linked);
$linkable_box = $this->newBox($linkable_head, $linkable);
return array(
$linked_box,
$linkable_box,
);
}
}
diff --git a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php
index 6809b5133..09193f3c9 100644
--- a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php
@@ -1,468 +1,468 @@
<?php
final class PhabricatorMultiFactorSettingsPanel
extends PhabricatorSettingsPanel {
private $isEnrollment;
public function getPanelKey() {
return 'multifactor';
}
public function getPanelName() {
return pht('Multi-Factor Auth');
}
public function getPanelMenuIcon() {
return 'fa-lock';
}
public function getPanelGroupKey() {
return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY;
}
public function isMultiFactorEnrollmentPanel() {
return true;
}
public function setIsEnrollment($is_enrollment) {
$this->isEnrollment = $is_enrollment;
return $this;
}
public function getIsEnrollment() {
return $this->isEnrollment;
}
public function processRequest(AphrontRequest $request) {
if ($request->getExists('new') || $request->getExists('providerPHID')) {
return $this->processNew($request);
}
if ($request->getExists('edit')) {
return $this->processEdit($request);
}
if ($request->getExists('delete')) {
return $this->processDelete($request);
}
$user = $this->getUser();
$viewer = $request->getUser();
$factors = id(new PhabricatorAuthFactorConfigQuery())
->setViewer($viewer)
->withUserPHIDs(array($user->getPHID()))
->execute();
$factors = msort($factors, 'newSortVector');
$rows = array();
$rowc = array();
$highlight_id = $request->getInt('id');
foreach ($factors as $factor) {
$provider = $factor->getFactorProvider();
if ($factor->getID() == $highlight_id) {
$rowc[] = 'highlighted';
} else {
$rowc[] = null;
}
$status = $provider->newStatus();
$status_icon = $status->getFactorIcon();
$status_color = $status->getFactorColor();
$icon = id(new PHUIIconView())
->setIcon("{$status_icon} {$status_color}")
->setTooltip(pht('Provider: %s', $status->getName()));
$details = $provider->getConfigurationListDetails($factor, $viewer);
$rows[] = array(
$icon,
javelin_tag(
'a',
array(
'href' => $this->getPanelURI('?edit='.$factor->getID()),
'sigil' => 'workflow',
),
$factor->getFactorName()),
$provider->getFactor()->getFactorShortName(),
$provider->getDisplayName(),
$details,
phabricator_datetime($factor->getDateCreated(), $viewer),
javelin_tag(
'a',
array(
'href' => $this->getPanelURI('?delete='.$factor->getID()),
'sigil' => 'workflow',
'class' => 'small button button-grey',
),
pht('Remove')),
);
}
$table = new AphrontTableView($rows);
$table->setNoDataString(
pht("You haven't added any authentication factors to your account yet."));
$table->setHeaders(
array(
null,
pht('Name'),
pht('Type'),
pht('Provider'),
pht('Details'),
pht('Created'),
null,
));
$table->setColumnClasses(
array(
null,
'wide pri',
null,
null,
null,
'right',
'action',
));
$table->setRowClasses($rowc);
$table->setDeviceVisibility(
array(
true,
true,
false,
false,
false,
false,
true,
));
$help_uri = PhabricatorEnv::getDoclink(
'User Guide: Multi-Factor Authentication');
$buttons = array();
// If we're enrolling a new account in MFA, provide a small visual hint
// that this is the button they want to click.
if ($this->getIsEnrollment()) {
$add_color = PHUIButtonView::BLUE;
} else {
$add_color = PHUIButtonView::GREY;
}
$can_add = (bool)$this->loadActiveMFAProviders();
$buttons[] = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-plus')
->setText(pht('Add Auth Factor'))
->setHref($this->getPanelURI('?new=true'))
->setWorkflow(true)
->setDisabled(!$can_add)
->setColor($add_color);
$buttons[] = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-book')
->setText(pht('Help'))
->setHref($help_uri)
->setColor(PHUIButtonView::GREY);
return $this->newBox(pht('Authentication Factors'), $table, $buttons);
}
private function processNew(AphrontRequest $request) {
$viewer = $request->getUser();
$user = $this->getUser();
$cancel_uri = $this->getPanelURI();
// Check that we have providers before we send the user through the MFA
// gate, so you don't authenticate and then immediately get roadblocked.
$providers = $this->loadActiveMFAProviders();
if (!$providers) {
return $this->newDialog()
->setTitle(pht('No MFA Providers'))
->appendParagraph(
pht(
'This install does not have any active MFA providers configured. '.
'At least one provider must be configured and active before you '.
'can add new MFA factors.'))
->addCancelButton($cancel_uri);
}
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$cancel_uri);
$selected_phid = $request->getStr('providerPHID');
if (empty($providers[$selected_phid])) {
$selected_provider = null;
} else {
$selected_provider = $providers[$selected_phid];
// Only let the user continue creating a factor for a given provider if
// they actually pass the provider's checks.
if (!$selected_provider->canCreateNewConfiguration($viewer)) {
$selected_provider = null;
}
}
if (!$selected_provider) {
$menu = id(new PHUIObjectItemListView())
->setViewer($viewer)
->setBig(true)
->setFlush(true);
foreach ($providers as $provider_phid => $provider) {
$provider_uri = id(new PhutilURI($this->getPanelURI()))
- ->setQueryParam('providerPHID', $provider_phid);
+ ->replaceQueryParam('providerPHID', $provider_phid);
$is_enabled = $provider->canCreateNewConfiguration($viewer);
$item = id(new PHUIObjectItemView())
->setHeader($provider->getDisplayName())
->setImageIcon($provider->newIconView())
->addAttribute($provider->getDisplayDescription());
if ($is_enabled) {
$item
->setHref($provider_uri)
->setClickable(true);
} else {
$item->setDisabled(true);
}
$create_description = $provider->getConfigurationCreateDescription(
$viewer);
if ($create_description) {
$item->appendChild($create_description);
}
$menu->addItem($item);
}
return $this->newDialog()
->setTitle(pht('Choose Factor Type'))
->appendChild($menu)
->addCancelButton($cancel_uri);
}
// NOTE: Beyond providing guidance, this step is also providing a CSRF gate
// on this endpoint, since prompting the user to respond to a challenge
// sometimes requires us to push a challenge to them as a side effect (for
// example, with SMS).
if (!$request->isFormPost() || !$request->getBool('mfa.start')) {
$enroll = $selected_provider->getEnrollMessage();
if (!strlen($enroll)) {
$enroll = $selected_provider->getEnrollDescription($viewer);
}
return $this->newDialog()
->addHiddenInput('providerPHID', $selected_provider->getPHID())
->addHiddenInput('mfa.start', 1)
->setTitle(pht('Add Authentication Factor'))
->appendChild(new PHUIRemarkupView($viewer, $enroll))
->addCancelButton($cancel_uri)
->addSubmitButton($selected_provider->getEnrollButtonText($viewer));
}
$form = id(new AphrontFormView())
->setViewer($viewer);
if ($request->getBool('mfa.enroll')) {
// Subject users to rate limiting so that it's difficult to add factors
// by pure brute force. This is normally not much of an attack, but push
// factor types may have side effects.
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorAuthNewFactorAction(),
1);
} else {
// Test the limit before showing the user a form, so we don't give them
// a form which can never possibly work because it will always hit rate
// limiting.
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorAuthNewFactorAction(),
0);
}
$config = $selected_provider->processAddFactorForm(
$form,
$request,
$user);
if ($config) {
// If the user added a factor, give them a rate limiting point back.
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorAuthNewFactorAction(),
-1);
$config->save();
// If we used a temporary token to handle synchronizing the factor,
// revoke it now.
$sync_token = $config->getMFASyncToken();
if ($sync_token) {
$sync_token->revokeToken();
}
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$user->getPHID(),
PhabricatorUserLog::ACTION_MULTI_ADD);
$log->save();
$user->updateMultiFactorEnrollment();
// Terminate other sessions so they must log in and survive the
// multi-factor auth check.
id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
$user,
new PhutilOpaqueEnvelope(
$request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?id='.$config->getID()));
}
return $this->newDialog()
->addHiddenInput('providerPHID', $selected_provider->getPHID())
->addHiddenInput('mfa.start', 1)
->addHiddenInput('mfa.enroll', 1)
->setWidth(AphrontDialogView::WIDTH_FORM)
->setTitle(pht('Add Authentication Factor'))
->appendChild($form->buildLayoutView())
->addSubmitButton(pht('Continue'))
->addCancelButton($cancel_uri);
}
private function processEdit(AphrontRequest $request) {
$viewer = $request->getUser();
$user = $this->getUser();
$factor = id(new PhabricatorAuthFactorConfig())->loadOneWhere(
'id = %d AND userPHID = %s',
$request->getInt('edit'),
$user->getPHID());
if (!$factor) {
return new Aphront404Response();
}
$e_name = true;
$errors = array();
if ($request->isFormPost()) {
$name = $request->getStr('name');
if (!strlen($name)) {
$e_name = pht('Required');
$errors[] = pht(
'Authentication factors must have a name to identify them.');
}
if (!$errors) {
$factor->setFactorName($name);
$factor->save();
$user->updateMultiFactorEnrollment();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?id='.$factor->getID()));
}
} else {
$name = $factor->getFactorName();
}
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormTextControl())
->setName('name')
->setLabel(pht('Name'))
->setValue($name)
->setError($e_name));
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addHiddenInput('edit', $factor->getID())
->setTitle(pht('Edit Authentication Factor'))
->setErrors($errors)
->appendChild($form->buildLayoutView())
->addSubmitButton(pht('Save'))
->addCancelButton($this->getPanelURI());
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
private function processDelete(AphrontRequest $request) {
$viewer = $request->getUser();
$user = $this->getUser();
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$this->getPanelURI());
$factor = id(new PhabricatorAuthFactorConfig())->loadOneWhere(
'id = %d AND userPHID = %s',
$request->getInt('delete'),
$user->getPHID());
if (!$factor) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
$factor->delete();
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$user->getPHID(),
PhabricatorUserLog::ACTION_MULTI_REMOVE);
$log->save();
$user->updateMultiFactorEnrollment();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI());
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addHiddenInput('delete', $factor->getID())
->setTitle(pht('Delete Authentication Factor'))
->appendParagraph(
pht(
'Really remove the authentication factor %s from your account?',
phutil_tag('strong', array(), $factor->getFactorName())))
->addSubmitButton(pht('Remove Factor'))
->addCancelButton($this->getPanelURI());
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
private function loadActiveMFAProviders() {
$viewer = $this->getViewer();
$providers = id(new PhabricatorAuthFactorProviderQuery())
->setViewer($viewer)
->withStatuses(
array(
PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
))
->execute();
$providers = mpull($providers, null, 'getPHID');
$providers = msortv($providers, 'newSortVector');
return $providers;
}
}
diff --git a/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php b/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php
index 37393d5d4..77f32f977 100644
--- a/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php
@@ -1,222 +1,244 @@
<?php
final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'password';
}
public function getPanelName() {
return pht('Password');
}
public function getPanelMenuIcon() {
return 'fa-key';
}
public function getPanelGroupKey() {
return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY;
}
public function isEnabled() {
// There's no sense in showing a change password panel if this install
// doesn't support password authentication.
if (!PhabricatorPasswordAuthProvider::getPasswordProvider()) {
return false;
}
return true;
}
public function processRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$user = $this->getUser();
$content_source = PhabricatorContentSource::newFromRequest($request);
- $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
- $viewer,
- $request,
- '/settings/');
-
$min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length');
$min_len = (int)$min_len;
// NOTE: Users can also change passwords through the separate "set/reset"
// interface which is reached by logging in with a one-time token after
// registration or password reset. If this flow changes, that flow may
// also need to change.
$account_type = PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT;
$password_objects = id(new PhabricatorAuthPasswordQuery())
->setViewer($viewer)
->withObjectPHIDs(array($user->getPHID()))
->withPasswordTypes(array($account_type))
->withIsRevoked(false)
->execute();
- if ($password_objects) {
- $password_object = head($password_objects);
- } else {
- $password_object = PhabricatorAuthPassword::initializeNewPassword(
- $user,
- $account_type);
+ if (!$password_objects) {
+ return $this->newSetPasswordView($request);
}
+ $password_object = head($password_objects);
$e_old = true;
$e_new = true;
$e_conf = true;
$errors = array();
- if ($request->isFormPost()) {
+ if ($request->isFormOrHisecPost()) {
+ $workflow_key = sprintf(
+ 'password.change(%s)',
+ $user->getPHID());
+
+ $hisec_token = id(new PhabricatorAuthSessionEngine())
+ ->setWorkflowKey($workflow_key)
+ ->requireHighSecurityToken($viewer, $request, '/settings/');
+
// Rate limit guesses about the old password. This page requires MFA and
// session compromise already, so this is mostly just to stop researchers
// from reporting this as a vulnerability.
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorAuthChangePasswordAction(),
1);
$envelope = new PhutilOpaqueEnvelope($request->getStr('old_pw'));
$engine = id(new PhabricatorAuthPasswordEngine())
->setViewer($viewer)
->setContentSource($content_source)
->setPasswordType($account_type)
->setObject($user);
if (!strlen($envelope->openEnvelope())) {
$errors[] = pht('You must enter your current password.');
$e_old = pht('Required');
} else if (!$engine->isValidPassword($envelope)) {
$errors[] = pht('The old password you entered is incorrect.');
$e_old = pht('Invalid');
} else {
$e_old = null;
// Refund the user an action credit for getting the password right.
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorAuthChangePasswordAction(),
-1);
}
$pass = $request->getStr('new_pw');
$conf = $request->getStr('conf_pw');
$password_envelope = new PhutilOpaqueEnvelope($pass);
$confirm_envelope = new PhutilOpaqueEnvelope($conf);
try {
$engine->checkNewPassword($password_envelope, $confirm_envelope);
$e_new = null;
$e_conf = null;
} catch (PhabricatorAuthPasswordException $ex) {
$errors[] = $ex->getMessage();
$e_new = $ex->getPasswordError();
$e_conf = $ex->getConfirmError();
}
if (!$errors) {
$password_object
->setPassword($password_envelope, $user)
->save();
$next = $this->getPanelURI('?saved=true');
id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
$user,
new PhutilOpaqueEnvelope(
$request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
return id(new AphrontRedirectResponse())->setURI($next);
}
}
if ($password_object->getID()) {
try {
$can_upgrade = $password_object->canUpgrade();
} catch (PhabricatorPasswordHasherUnavailableException $ex) {
$can_upgrade = false;
$errors[] = pht(
'Your password is currently hashed using an algorithm which is '.
'no longer available on this install.');
$errors[] = pht(
'Because the algorithm implementation is missing, your password '.
'can not be used or updated.');
$errors[] = pht(
'To set a new password, request a password reset link from the '.
'login screen and then follow the instructions.');
}
if ($can_upgrade) {
$errors[] = pht(
'The strength of your stored password hash can be upgraded. '.
'To upgrade, either: log out and log in using your password; or '.
'change your password.');
}
}
$len_caption = null;
if ($min_len) {
$len_caption = pht('Minimum password length: %d characters.', $min_len);
}
$form = id(new AphrontFormView())
->setViewer($viewer)
->appendChild(
id(new AphrontFormPasswordControl())
->setLabel(pht('Old Password'))
->setError($e_old)
->setName('old_pw'))
->appendChild(
id(new AphrontFormPasswordControl())
->setDisableAutocomplete(true)
->setLabel(pht('New Password'))
->setError($e_new)
->setName('new_pw'))
->appendChild(
id(new AphrontFormPasswordControl())
->setDisableAutocomplete(true)
->setLabel(pht('Confirm Password'))
->setCaption($len_caption)
->setError($e_conf)
->setName('conf_pw'))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Change Password')));
$properties = id(new PHUIPropertyListView());
$properties->addProperty(
pht('Current Algorithm'),
PhabricatorPasswordHasher::getCurrentAlgorithmName(
$password_object->newPasswordEnvelope()));
$properties->addProperty(
pht('Best Available Algorithm'),
PhabricatorPasswordHasher::getBestAlgorithmName());
$info_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->appendChild(
pht('Changing your password will terminate any other outstanding '.
'login sessions.'));
$algo_box = $this->newBox(pht('Password Algorithms'), $properties);
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Change Password'))
->setFormSaved($request->getStr('saved'))
->setFormErrors($errors)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->setForm($form);
return array(
$form_box,
$algo_box,
$info_view,
);
}
+ private function newSetPasswordView(AphrontRequest $request) {
+ $viewer = $request->getUser();
+ $user = $this->getUser();
+
+ $form = id(new AphrontFormView())
+ ->setViewer($viewer)
+ ->appendRemarkupInstructions(
+ pht(
+ 'Your account does not currently have a password set. You can '.
+ 'choose a password by performing a password reset.'))
+ ->appendControl(
+ id(new AphrontFormSubmitControl())
+ ->addCancelButton('/login/email/', pht('Reset Password')));
+
+ $form_box = id(new PHUIObjectBoxView())
+ ->setHeaderText(pht('Set Password'))
+ ->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
+ ->setForm($form);
+
+ return $form_box;
+ }
+
}
diff --git a/src/applications/settings/setting/PhabricatorTimezoneSetting.php b/src/applications/settings/setting/PhabricatorTimezoneSetting.php
index 887e08129..52fce7742 100644
--- a/src/applications/settings/setting/PhabricatorTimezoneSetting.php
+++ b/src/applications/settings/setting/PhabricatorTimezoneSetting.php
@@ -1,102 +1,105 @@
<?php
final class PhabricatorTimezoneSetting
extends PhabricatorOptionGroupSetting {
const SETTINGKEY = 'timezone';
public function getSettingName() {
return pht('Timezone');
}
public function getSettingPanelKey() {
return PhabricatorDateTimeSettingsPanel::PANELKEY;
}
protected function getSettingOrder() {
return 100;
}
protected function getControlInstructions() {
return pht('Select your local timezone.');
}
public function getSettingDefaultValue() {
return date_default_timezone_get();
}
public function assertValidValue($value) {
// NOTE: This isn't doing anything fancy, it's just a much faster
// validator than doing all the timezone calculations to build the full
// list of options.
if (!$value) {
return;
}
static $identifiers;
if ($identifiers === null) {
$identifiers = DateTimeZone::listIdentifiers();
$identifiers = array_fuse($identifiers);
}
if (isset($identifiers[$value])) {
return;
}
throw new Exception(
pht(
'Timezone "%s" is not a valid timezone identifier.',
$value));
}
protected function getSelectOptionGroups() {
$timezones = DateTimeZone::listIdentifiers();
$now = new DateTime('@'.PhabricatorTime::getNow());
$groups = array();
foreach ($timezones as $timezone) {
$zone = new DateTimeZone($timezone);
- $offset = -($zone->getOffset($now) / (60 * 60));
+ $offset = ($zone->getOffset($now) / 60);
$groups[$offset][] = $timezone;
}
- krsort($groups);
+ ksort($groups);
$option_groups = array(
array(
'label' => pht('Default'),
'options' => array(),
),
);
foreach ($groups as $offset => $group) {
- if ($offset >= 0) {
- $label = pht('UTC-%d', $offset);
+ $hours = $offset / 60;
+ $minutes = abs($offset % 60);
+
+ if ($offset % 60) {
+ $label = pht('UTC%+d:%02d', $hours, $minutes);
} else {
- $label = pht('UTC+%d', -$offset);
+ $label = pht('UTC%+d', $hours);
}
sort($group);
$option_groups[] = array(
'label' => $label,
'options' => array_fuse($group),
);
}
return $option_groups;
}
public function expandSettingTransaction($object, $xaction) {
// When the user changes their timezone, we also clear any ignored
// timezone offset.
return array(
$xaction,
$this->newSettingTransaction(
$object,
PhabricatorTimezoneIgnoreOffsetSetting::SETTINGKEY,
null),
);
}
}
diff --git a/src/applications/settings/storage/PhabricatorUserPreferencesTransaction.php b/src/applications/settings/storage/PhabricatorUserPreferencesTransaction.php
index 6378ee29d..3ef48c01e 100644
--- a/src/applications/settings/storage/PhabricatorUserPreferencesTransaction.php
+++ b/src/applications/settings/storage/PhabricatorUserPreferencesTransaction.php
@@ -1,22 +1,18 @@
<?php
final class PhabricatorUserPreferencesTransaction
extends PhabricatorApplicationTransaction {
const TYPE_SETTING = 'setting';
const PROPERTY_SETTING = 'setting.key';
public function getApplicationName() {
return 'user';
}
- public function getApplicationTransactionCommentObject() {
- return null;
- }
-
public function getApplicationTransactionType() {
return PhabricatorUserPreferencesPHIDType::TYPECONST;
}
}
diff --git a/src/applications/slowvote/controller/PhabricatorSlowvoteVoteController.php b/src/applications/slowvote/controller/PhabricatorSlowvoteVoteController.php
index 62913b09e..e1a1b9df3 100644
--- a/src/applications/slowvote/controller/PhabricatorSlowvoteVoteController.php
+++ b/src/applications/slowvote/controller/PhabricatorSlowvoteVoteController.php
@@ -1,77 +1,106 @@
<?php
final class PhabricatorSlowvoteVoteController
extends PhabricatorSlowvoteController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
if (!$request->isFormPost()) {
return id(new Aphront404Response());
}
$poll = id(new PhabricatorSlowvoteQuery())
->setViewer($viewer)
->withIDs(array($id))
->needOptions(true)
->needViewerChoices(true)
->executeOne();
if (!$poll) {
return new Aphront404Response();
}
if ($poll->getIsClosed()) {
return new Aphront400Response();
}
$options = $poll->getOptions();
$options = mpull($options, null, 'getID');
$old_votes = $poll->getViewerChoices($viewer);
$old_votes = mpull($old_votes, null, 'getOptionID');
$votes = $request->getArr('vote');
$votes = array_fuse($votes);
$method = $poll->getMethod();
$is_plurality = ($method == PhabricatorSlowvotePoll::METHOD_PLURALITY);
+ if (!$votes) {
+ if ($is_plurality) {
+ $message = pht('You must vote for something.');
+ } else {
+ $message = pht('You must vote for at least one option.');
+ }
+
+ return $this->newDialog()
+ ->setTitle(pht('Stand For Something'))
+ ->appendParagraph($message)
+ ->addCancelButton($poll->getURI());
+ }
+
if ($is_plurality && count($votes) > 1) {
throw new Exception(
pht('In this poll, you may only vote for one option.'));
}
foreach ($votes as $vote) {
if (!isset($options[$vote])) {
throw new Exception(
pht(
'Option ("%s") is not a valid poll option. You may only '.
'vote for valid options.',
$vote));
}
}
- foreach ($old_votes as $old_vote) {
- if (!idx($votes, $old_vote->getOptionID(), false)) {
+ $poll->openTransaction();
+ $poll->beginReadLocking();
+
+ $poll->reload();
+
+ $old_votes = id(new PhabricatorSlowvoteChoice())->loadAllWhere(
+ 'pollID = %d AND authorPHID = %s',
+ $poll->getID(),
+ $viewer->getPHID());
+ $old_votes = mpull($old_votes, null, 'getOptionID');
+
+ foreach ($old_votes as $old_vote) {
+ if (idx($votes, $old_vote->getOptionID())) {
+ continue;
+ }
+
$old_vote->delete();
}
- }
- foreach ($votes as $vote) {
- if (idx($old_votes, $vote, false)) {
- continue;
+ foreach ($votes as $vote) {
+ if (idx($old_votes, $vote)) {
+ continue;
+ }
+
+ id(new PhabricatorSlowvoteChoice())
+ ->setAuthorPHID($viewer->getPHID())
+ ->setPollID($poll->getID())
+ ->setOptionID($vote)
+ ->save();
}
- id(new PhabricatorSlowvoteChoice())
- ->setAuthorPHID($viewer->getPHID())
- ->setPollID($poll->getID())
- ->setOptionID($vote)
- ->save();
- }
+ $poll->endReadLocking();
+ $poll->saveTransaction();
return id(new AphrontRedirectResponse())
->setURI($poll->getURI());
}
}
diff --git a/src/applications/spaces/storage/PhabricatorSpacesNamespaceTransaction.php b/src/applications/spaces/storage/PhabricatorSpacesNamespaceTransaction.php
index 0f50a870f..bac0ea636 100644
--- a/src/applications/spaces/storage/PhabricatorSpacesNamespaceTransaction.php
+++ b/src/applications/spaces/storage/PhabricatorSpacesNamespaceTransaction.php
@@ -1,22 +1,18 @@
<?php
final class PhabricatorSpacesNamespaceTransaction
extends PhabricatorModularTransaction {
public function getApplicationName() {
return 'spaces';
}
public function getApplicationTransactionType() {
return PhabricatorSpacesNamespacePHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return null;
- }
-
public function getBaseTransactionClass() {
return 'PhabricatorSpacesNamespaceTransactionType';
}
}
diff --git a/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php b/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php
index caf860117..2077160b7 100644
--- a/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php
+++ b/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php
@@ -1,157 +1,153 @@
<?php
final class PhabricatorSubscriptionsUIEventListener
extends PhabricatorEventListener {
public function register() {
$this->listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS);
$this->listen(PhabricatorEventType::TYPE_UI_WILLRENDERPROPERTIES);
}
public function handleEvent(PhutilEvent $event) {
$object = $event->getValue('object');
switch ($event->getType()) {
case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS:
$this->handleActionEvent($event);
break;
case PhabricatorEventType::TYPE_UI_WILLRENDERPROPERTIES:
// Hacky solution so that property list view on Diffusion
// commits shows build status, but not Projects, Subscriptions,
// or Tokens.
if ($object instanceof PhabricatorRepositoryCommit) {
return;
}
$this->handlePropertyEvent($event);
break;
}
}
private function handleActionEvent($event) {
$user = $event->getUser();
$user_phid = $user->getPHID();
$object = $event->getValue('object');
if (!$object || !$object->getPHID()) {
// No object, or the object has no PHID yet. No way to subscribe.
return;
}
if (!($object instanceof PhabricatorSubscribableInterface)) {
// This object isn't subscribable.
return;
}
$src_phid = $object->getPHID();
$subscribed_type = PhabricatorObjectHasSubscriberEdgeType::EDGECONST;
$muted_type = PhabricatorMutedByEdgeType::EDGECONST;
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($src_phid))
->withEdgeTypes(
array(
$subscribed_type,
$muted_type,
))
->withDestinationPHIDs(array($user_phid))
->execute();
if ($user_phid) {
$is_subscribed = isset($edges[$src_phid][$subscribed_type][$user_phid]);
$is_muted = isset($edges[$src_phid][$muted_type][$user_phid]);
} else {
$is_subscribed = false;
$is_muted = false;
}
if ($user_phid && $object->isAutomaticallySubscribed($user_phid)) {
$sub_action = id(new PhabricatorActionView())
->setWorkflow(true)
->setDisabled(true)
->setRenderAsForm(true)
->setHref('/subscriptions/add/'.$object->getPHID().'/')
->setName(pht('Automatically Subscribed'))
->setIcon('fa-check-circle lightgreytext');
} else {
- $can_interact = PhabricatorPolicyFilter::canInteract($user, $object);
-
if ($is_subscribed) {
$sub_action = id(new PhabricatorActionView())
->setWorkflow(true)
->setRenderAsForm(true)
->setHref('/subscriptions/delete/'.$object->getPHID().'/')
->setName(pht('Unsubscribe'))
- ->setIcon('fa-minus-circle')
- ->setDisabled(!$can_interact);
+ ->setIcon('fa-minus-circle');
} else {
$sub_action = id(new PhabricatorActionView())
->setWorkflow(true)
->setRenderAsForm(true)
->setHref('/subscriptions/add/'.$object->getPHID().'/')
->setName(pht('Subscribe'))
- ->setIcon('fa-plus-circle')
- ->setDisabled(!$can_interact);
+ ->setIcon('fa-plus-circle');
}
if (!$user->isLoggedIn()) {
$sub_action->setDisabled(true);
}
}
$mute_action = id(new PhabricatorActionView())
->setWorkflow(true)
->setHref('/subscriptions/mute/'.$object->getPHID().'/')
->setDisabled(!$user_phid);
if (!$is_muted) {
$mute_action
->setName(pht('Mute Notifications'))
->setIcon('fa-volume-up');
} else {
$mute_action
->setName(pht('Unmute Notifications'))
->setIcon('fa-volume-off')
->setColor(PhabricatorActionView::RED);
}
$actions = $event->getValue('actions');
$actions[] = $sub_action;
$actions[] = $mute_action;
$event->setValue('actions', $actions);
}
private function handlePropertyEvent($event) {
$user = $event->getUser();
$object = $event->getValue('object');
if (!$object || !$object->getPHID()) {
// No object, or the object has no PHID yet..
return;
}
if (!($object instanceof PhabricatorSubscribableInterface)) {
// This object isn't subscribable.
return;
}
$subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID(
$object->getPHID());
if ($subscribers) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs($subscribers)
->execute();
} else {
$handles = array();
}
$sub_view = id(new SubscriptionListStringBuilder())
->setObjectPHID($object->getPHID())
->setHandles($handles)
->buildPropertyString();
$view = $event->getValue('view');
$view->addProperty(pht('Subscribers'), $sub_view);
}
}
diff --git a/src/applications/system/engine/PhabricatorDefaultUnlockEngine.php b/src/applications/system/engine/PhabricatorDefaultUnlockEngine.php
new file mode 100644
index 000000000..624191ad2
--- /dev/null
+++ b/src/applications/system/engine/PhabricatorDefaultUnlockEngine.php
@@ -0,0 +1,4 @@
+<?php
+
+final class PhabricatorDefaultUnlockEngine
+ extends PhabricatorUnlockEngine {}
diff --git a/src/applications/system/engine/PhabricatorUnlockEngine.php b/src/applications/system/engine/PhabricatorUnlockEngine.php
new file mode 100644
index 000000000..8afcc2873
--- /dev/null
+++ b/src/applications/system/engine/PhabricatorUnlockEngine.php
@@ -0,0 +1,81 @@
+<?php
+
+abstract class PhabricatorUnlockEngine
+ extends Phobject {
+
+ final public static function newUnlockEngineForObject($object) {
+ if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
+ throw new Exception(
+ pht(
+ 'Object ("%s") does not implement interface "%s", so this type '.
+ 'of object can not be unlocked.',
+ phutil_describe_type($object),
+ 'PhabricatorApplicationTransactionInterface'));
+ }
+
+ if ($object instanceof PhabricatorUnlockableInterface) {
+ $engine = $object->newUnlockEngine();
+ } else {
+ $engine = new PhabricatorDefaultUnlockEngine();
+ }
+
+ return $engine;
+ }
+
+ public function newUnlockViewTransactions($object, $user) {
+ $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY;
+
+ if (!$this->canApplyTransactionType($object, $type_view)) {
+ throw new Exception(
+ pht(
+ 'Object view policy can not be unlocked because this object '.
+ 'does not have a mutable view policy.'));
+ }
+
+ return array(
+ $this->newTransaction($object)
+ ->setTransactionType($type_view)
+ ->setNewValue($user->getPHID()),
+ );
+ }
+
+ public function newUnlockEditTransactions($object, $user) {
+ $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY;
+
+ if (!$this->canApplyTransactionType($object, $type_edit)) {
+ throw new Exception(
+ pht(
+ 'Object edit policy can not be unlocked because this object '.
+ 'does not have a mutable edit policy.'));
+ }
+
+ return array(
+ $this->newTransaction($object)
+ ->setTransactionType($type_edit)
+ ->setNewValue($user->getPHID()),
+ );
+ }
+
+ public function newUnlockOwnerTransactions($object, $user) {
+ throw new Exception(
+ pht(
+ 'Object owner can not be unlocked: the unlocking engine ("%s") for '.
+ 'this object does not implement an owner unlocking mechanism.',
+ get_class($this)));
+ }
+
+ final protected function canApplyTransactionType($object, $type) {
+ $xaction_types = $object->getApplicationTransactionEditor()
+ ->getTransactionTypesForObject($object);
+
+ $xaction_types = array_fuse($xaction_types);
+
+ return isset($xaction_types[$type]);
+ }
+
+ final protected function newTransaction($object) {
+ return $object->getApplicationTransactionTemplate();
+ }
+
+
+}
diff --git a/src/applications/system/interface/PhabricatorUnlockableInterface.php b/src/applications/system/interface/PhabricatorUnlockableInterface.php
new file mode 100644
index 000000000..1a95215e8
--- /dev/null
+++ b/src/applications/system/interface/PhabricatorUnlockableInterface.php
@@ -0,0 +1,18 @@
+<?php
+
+interface PhabricatorUnlockableInterface {
+
+ public function newUnlockEngine();
+
+}
+
+// TEMPLATE IMPLEMENTATION /////////////////////////////////////////////////////
+
+/* -( PhabricatorUnlockableInterface )------------------------------------- */
+/*
+
+ public function newUnlockEngine() {
+ return new <<<...>>>UnlockEngine();
+ }
+
+*/
diff --git a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php
index 0edc0b3f5..4ab5de519 100644
--- a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php
+++ b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php
@@ -1,243 +1,343 @@
<?php
final class TransactionSearchConduitAPIMethod
extends ConduitAPIMethod {
public function getAPIMethodName() {
return 'transaction.search';
}
public function getMethodDescription() {
- return pht('Read transactions for an object.');
+ return pht('Read transactions and comments for an object.');
}
- public function getMethodStatus() {
- return self::METHOD_STATUS_UNSTABLE;
- }
+ public function getMethodDocumentation() {
+ $markup = pht(<<<EOREMARKUP
+When an object (like a task) is edited, Phabricator creates a "transaction"
+and applies it. This list of transactions on each object is the basis for
+essentially all edits and comments in Phabricator. Reviewing the transaction
+record allows you to see who edited an object, when, and how their edit changed
+things.
+
+One common reason to call this method is that you're implmenting a webhook and
+just received a notification that an object has changed. See the Webhooks
+documentation for more detailed discussion of this use case.
+
+Constraints
+===========
+
+These constraints are supported:
+
+ - `phids` //Optional list<phid>.// Find specific transactions by PHID. This
+ is most likely to be useful if you're responding to a webhook notification
+ and want to inspect only the related events.
+ - `authorPHIDs` //Optional list<phid>.// Find transactions with particular
+ authors.
+
+Transaction Format
+==================
+
+Each transaction has custom data describing what the transaction did. The
+format varies from transaction to transaction. The easiest way to figure out
+exactly what a particular transaction looks like is to make the associated kind
+of edit to a test object, then query that object.
+
+Not all transactions have data: by default, transactions have a `null` "type"
+and no additional data. This API does not expose raw transaction data because
+some of it is internal, oddly named, misspelled, confusing, not useful, or
+could create security or policy problems to expose directly.
+
+New transactions are exposed (with correctly spelled, comprehensible types and
+useful, reasonable fields) as we become aware of use cases for them.
- public function getMethodStatusDescription() {
- return pht('This method is new and experimental.');
+EOREMARKUP
+ );
+
+ $markup = $this->newRemarkupDocumentationView($markup);
+
+ return id(new PHUIObjectBoxView())
+ ->setCollapsed(true)
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->setHeaderText(pht('Method Details'))
+ ->appendChild($markup);
}
protected function defineParamTypes() {
return array(
'objectIdentifier' => 'phid|string',
'constraints' => 'map<string, wild>',
) + $this->getPagerParamTypes();
}
protected function defineReturnType() {
return 'list<dict>';
}
protected function defineErrorTypes() {
return array();
}
protected function execute(ConduitAPIRequest $request) {
$viewer = $request->getUser();
$pager = $this->newPager($request);
$object_name = $request->getValue('objectIdentifier', null);
if (!strlen($object_name)) {
throw new Exception(
pht(
'When calling "transaction.search", you must provide an object to '.
'retrieve transactions for.'));
}
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withNames(array($object_name))
->executeOne();
if (!$object) {
throw new Exception(
pht(
'No object "%s" exists.',
$object_name));
}
if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
throw new Exception(
pht(
'Object "%s" does not implement "%s", so transactions can not '.
'be loaded for it.'));
}
$xaction_query = PhabricatorApplicationTransactionQuery::newQueryForObject(
$object);
$xaction_query
->needHandles(false)
->withObjectPHIDs(array($object->getPHID()))
->setViewer($viewer);
$constraints = $request->getValue('constraints', array());
- PhutilTypeSpec::checkMap(
- $constraints,
- array(
- 'phids' => 'optional list<string>',
- ));
- $with_phids = idx($constraints, 'phids');
-
- if ($with_phids === array()) {
- throw new Exception(
- pht(
- 'Constraint "phids" to "transaction.search" requires nonempty list, '.
- 'empty list provided.'));
- }
-
- if ($with_phids) {
- $xaction_query->withPHIDs($with_phids);
- }
+ $xaction_query = $this->applyConstraints($constraints, $xaction_query);
$xactions = $xaction_query->executeWithCursorPager($pager);
$comment_map = array();
if ($xactions) {
$template = head($xactions)->getApplicationTransactionCommentObject();
if ($template) {
$query = new PhabricatorApplicationTransactionTemplatedCommentQuery();
$comment_map = $query
->setViewer($viewer)
->setTemplate($template)
->withTransactionPHIDs(mpull($xactions, 'getPHID'))
->execute();
$comment_map = msort($comment_map, 'getCommentVersion');
$comment_map = array_reverse($comment_map);
$comment_map = mgroup($comment_map, 'getTransactionPHID');
}
}
$modular_classes = array();
$modular_objects = array();
$modular_xactions = array();
foreach ($xactions as $xaction) {
if (!$xaction instanceof PhabricatorModularTransaction) {
continue;
}
// TODO: Hack things so certain transactions which don't have a modular
// type yet can use a pseudotype until they modularize. Some day, we'll
// modularize everything and remove this.
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
$modular_template = new DifferentialRevisionInlineTransaction();
break;
default:
$modular_template = $xaction->getModularType();
break;
}
$modular_class = get_class($modular_template);
if (!isset($modular_objects[$modular_class])) {
try {
$modular_object = newv($modular_class, array());
$modular_objects[$modular_class] = $modular_object;
} catch (Exception $ex) {
continue;
}
}
$modular_classes[$xaction->getPHID()] = $modular_class;
$modular_xactions[$modular_class][] = $xaction;
}
$modular_data_map = array();
foreach ($modular_objects as $class => $modular_type) {
$modular_data_map[$class] = $modular_type
->setViewer($viewer)
->loadTransactionTypeConduitData($modular_xactions[$class]);
}
$data = array();
foreach ($xactions as $xaction) {
$comments = idx($comment_map, $xaction->getPHID());
$comment_data = array();
if ($comments) {
$removed = head($comments)->getIsDeleted();
foreach ($comments as $comment) {
if ($removed) {
// If the most recent version of the comment has been removed,
// don't show the history. This is for consistency with the web
// UI, which also prevents users from retrieving the content of
// removed comments.
$content = array(
'raw' => '',
);
} else {
$content = array(
'raw' => (string)$comment->getContent(),
);
}
$comment_data[] = array(
'id' => (int)$comment->getID(),
'phid' => (string)$comment->getPHID(),
'version' => (int)$comment->getCommentVersion(),
'authorPHID' => (string)$comment->getAuthorPHID(),
'dateCreated' => (int)$comment->getDateCreated(),
'dateModified' => (int)$comment->getDateModified(),
'removed' => (bool)$comment->getIsDeleted(),
'content' => $content,
);
}
}
$fields = array();
$type = null;
if (isset($modular_classes[$xaction->getPHID()])) {
$modular_class = $modular_classes[$xaction->getPHID()];
$modular_object = $modular_objects[$modular_class];
$modular_data = $modular_data_map[$modular_class];
$type = $modular_object->getTransactionTypeForConduit($xaction);
$fields = $modular_object->getFieldValuesForConduit(
$xaction,
$modular_data);
}
if (!$fields) {
$fields = (object)$fields;
}
// If we haven't found a modular type, fallback for some simple core
// types. Ideally, we'll modularize everything some day.
if ($type === null) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$type = 'comment';
break;
case PhabricatorTransactions::TYPE_CREATE:
$type = 'create';
break;
+ case PhabricatorTransactions::TYPE_EDGE:
+ switch ($xaction->getMetadataValue('edge:type')) {
+ case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST:
+ $type = 'projects';
+ $fields = $this->newEdgeTransactionFields($xaction);
+ break;
+ }
+ break;
}
}
$data[] = array(
'id' => (int)$xaction->getID(),
'phid' => (string)$xaction->getPHID(),
'type' => $type,
'authorPHID' => (string)$xaction->getAuthorPHID(),
'objectPHID' => (string)$xaction->getObjectPHID(),
'dateCreated' => (int)$xaction->getDateCreated(),
'dateModified' => (int)$xaction->getDateModified(),
'comments' => $comment_data,
'fields' => $fields,
);
}
$results = array(
'data' => $data,
);
return $this->addPagerResults($results, $pager);
}
+
+ private function applyConstraints(
+ array $constraints,
+ PhabricatorApplicationTransactionQuery $query) {
+
+ PhutilTypeSpec::checkMap(
+ $constraints,
+ array(
+ 'phids' => 'optional list<string>',
+ 'authorPHIDs' => 'optional list<string>',
+ ));
+
+ $with_phids = idx($constraints, 'phids');
+
+ if ($with_phids === array()) {
+ throw new Exception(
+ pht(
+ 'Constraint "phids" to "transaction.search" requires nonempty list, '.
+ 'empty list provided.'));
+ }
+
+ if ($with_phids) {
+ $query->withPHIDs($with_phids);
+ }
+
+ $with_authors = idx($constraints, 'authorPHIDs');
+ if ($with_authors === array()) {
+ throw new Exception(
+ pht(
+ 'Constraint "authorPHIDs" to "transaction.search" requires '.
+ 'nonempty list, empty list provided.'));
+ }
+
+ if ($with_authors) {
+ $query->withAuthorPHIDs($with_authors);
+ }
+
+ return $query;
+ }
+
+ private function newEdgeTransactionFields(
+ PhabricatorApplicationTransaction $xaction) {
+
+ $record = PhabricatorEdgeChangeRecord::newFromTransaction($xaction);
+
+ $operations = array();
+ foreach ($record->getAddedPHIDs() as $phid) {
+ $operations[] = array(
+ 'operation' => 'add',
+ 'phid' => $phid,
+ );
+ }
+
+ foreach ($record->getRemovedPHIDs() as $phid) {
+ $operations[] = array(
+ 'operation' => 'remove',
+ 'phid' => $phid,
+ );
+ }
+
+ return array(
+ 'operations' => $operations,
+ );
+ }
+
}
diff --git a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php
index a93f16a68..1682a7d13 100644
--- a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php
+++ b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php
@@ -1,69 +1,84 @@
<?php
final class PhabricatorApplicationTransactionCommentEditController
extends PhabricatorApplicationTransactionController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$xaction = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($request->getURIData('phid')))
->executeOne();
if (!$xaction) {
return new Aphront404Response();
}
if (!$xaction->getComment()) {
// You can't currently edit a transaction which doesn't have a comment.
// Some day you may be able to edit the visibility.
return new Aphront404Response();
}
if ($xaction->getComment()->getIsRemoved()) {
// You can't edit history of a transaction with a removed comment.
return new Aphront400Response();
}
$phid = $xaction->getObjectPHID();
$handles = $viewer->loadHandles(array($phid));
$obj_handle = $handles[$phid];
- if ($request->isDialogFormPost()) {
+ $done_uri = $obj_handle->getURI();
+
+ if ($request->isFormOrHisecPost()) {
$text = $request->getStr('text');
$comment = $xaction->getApplicationTransactionCommentObject();
$comment->setContent($text);
if (!strlen($text)) {
$comment->setIsDeleted(true);
}
$editor = id(new PhabricatorApplicationTransactionCommentEditor())
->setActor($viewer)
->setContentSource(PhabricatorContentSource::newFromRequest($request))
+ ->setRequest($request)
+ ->setCancelURI($done_uri)
->applyEdit($xaction, $comment);
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())->setContent(array());
} else {
- return id(new AphrontReloadResponse())->setURI($obj_handle->getURI());
+ return id(new AphrontReloadResponse())->setURI($done_uri);
}
}
+ $errors = array();
+ if ($xaction->getIsMFATransaction()) {
+ $message = pht(
+ 'This comment was signed with MFA, so you will be required to '.
+ 'provide MFA credentials to make changes.');
+
+ $errors[] = id(new PHUIInfoView())
+ ->setSeverity(PHUIInfoView::SEVERITY_MFA)
+ ->setErrors(array($message));
+ }
+
$form = id(new AphrontFormView())
->setUser($viewer)
->setFullWidth(true)
->appendControl(
id(new PhabricatorRemarkupControl())
- ->setName('text')
- ->setValue($xaction->getComment()->getContent()));
+ ->setName('text')
+ ->setValue($xaction->getComment()->getContent()));
return $this->newDialog()
->setTitle(pht('Edit Comment'))
- ->addHiddenInput('anchor', $request->getStr('anchor'))
+ ->appendChild($errors)
->appendForm($form)
->addSubmitButton(pht('Save Changes'))
- ->addCancelButton($obj_handle->getURI());
+ ->addCancelButton($done_uri);
}
}
diff --git a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php
index c52b08727..381dfe117 100644
--- a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php
+++ b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php
@@ -1,73 +1,76 @@
<?php
final class PhabricatorApplicationTransactionCommentRemoveController
extends PhabricatorApplicationTransactionController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$phid = $request->getURIData('phid');
$xaction = id(new PhabricatorObjectQuery())
->withPHIDs(array($phid))
->setViewer($viewer)
->executeOne();
if (!$xaction) {
return new Aphront404Response();
}
if (!$xaction->getComment()) {
return new Aphront404Response();
}
if ($xaction->getComment()->getIsRemoved()) {
// You can't remove an already-removed comment.
return new Aphront400Response();
}
$obj_phid = $xaction->getObjectPHID();
$obj_handle = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array($obj_phid))
->executeOne();
- if ($request->isDialogFormPost()) {
+ $done_uri = $obj_handle->getURI();
+
+ if ($request->isFormOrHisecPost()) {
$comment = $xaction->getApplicationTransactionCommentObject()
->setContent('')
->setIsRemoved(true);
$editor = id(new PhabricatorApplicationTransactionCommentEditor())
->setActor($viewer)
+ ->setRequest($request)
+ ->setCancelURI($done_uri)
->setContentSource(PhabricatorContentSource::newFromRequest($request))
->applyEdit($xaction, $comment);
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())->setContent(array());
} else {
- return id(new AphrontReloadResponse())->setURI($obj_handle->getURI());
+ return id(new AphrontReloadResponse())->setURI($done_uri);
}
}
$form = id(new AphrontFormView())
->setUser($viewer);
$dialog = $this->newDialog()
->setTitle(pht('Remove Comment'));
$dialog
- ->addHiddenInput('anchor', $request->getStr('anchor'))
->appendParagraph(
pht(
"Removing a comment prevents anyone (including you) from reading ".
"it. Removing a comment also hides the comment's edit history ".
"and prevents it from being edited."))
->appendParagraph(
pht('Really remove this comment?'));
$dialog
->addSubmitButton(pht('Remove Comment'))
- ->addCancelButton($obj_handle->getURI());
+ ->addCancelButton($done_uri);
return $dialog;
}
}
diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php
index 465738687..acdcc8815 100644
--- a/src/applications/transactions/editengine/PhabricatorEditEngine.php
+++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php
@@ -1,2674 +1,2687 @@
<?php
/**
* @task fields Managing Fields
* @task text Display Text
* @task config Edit Engine Configuration
* @task uri Managing URIs
* @task load Creating and Loading Objects
* @task web Responding to Web Requests
* @task edit Responding to Edit Requests
* @task http Responding to HTTP Parameter Requests
* @task conduit Responding to Conduit Requests
*/
abstract class PhabricatorEditEngine
extends Phobject
implements PhabricatorPolicyInterface {
const EDITENGINECONFIG_DEFAULT = 'default';
const SUBTYPE_DEFAULT = 'default';
private $viewer;
private $controller;
private $isCreate;
private $editEngineConfiguration;
private $contextParameters = array();
private $targetObject;
private $page;
private $pages;
private $navigation;
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
final public function getViewer() {
return $this->viewer;
}
final public function setController(PhabricatorController $controller) {
$this->controller = $controller;
$this->setViewer($controller->getViewer());
return $this;
}
final public function getController() {
return $this->controller;
}
final public function getEngineKey() {
$key = $this->getPhobjectClassConstant('ENGINECONST', 64);
if (strpos($key, '/') !== false) {
throw new Exception(
pht(
'EditEngine ("%s") contains an invalid key character "/".',
get_class($this)));
}
return $key;
}
final public function getApplication() {
$app_class = $this->getEngineApplicationClass();
return PhabricatorApplication::getByClass($app_class);
}
final public function addContextParameter($key) {
$this->contextParameters[] = $key;
return $this;
}
public function isEngineConfigurable() {
return true;
}
public function isEngineExtensible() {
return true;
}
public function isDefaultQuickCreateEngine() {
return false;
}
public function getDefaultQuickCreateFormKeys() {
$keys = array();
if ($this->isDefaultQuickCreateEngine()) {
$keys[] = self::EDITENGINECONFIG_DEFAULT;
}
foreach ($keys as $idx => $key) {
$keys[$idx] = $this->getEngineKey().'/'.$key;
}
return $keys;
}
public static function splitFullKey($full_key) {
return explode('/', $full_key, 2);
}
public function getQuickCreateOrderVector() {
return id(new PhutilSortVector())
->addString($this->getObjectCreateShortText());
}
/**
* Force the engine to edit a particular object.
*/
public function setTargetObject($target_object) {
$this->targetObject = $target_object;
return $this;
}
public function getTargetObject() {
return $this->targetObject;
}
public function setNavigation(AphrontSideNavFilterView $navigation) {
$this->navigation = $navigation;
return $this;
}
public function getNavigation() {
return $this->navigation;
}
/* -( Managing Fields )---------------------------------------------------- */
abstract public function getEngineApplicationClass();
abstract protected function buildCustomEditFields($object);
public function getFieldsForConfig(
PhabricatorEditEngineConfiguration $config) {
$object = $this->newEditableObject();
$this->editEngineConfiguration = $config;
// This is mostly making sure that we fill in default values.
$this->setIsCreate(true);
return $this->buildEditFields($object);
}
final protected function buildEditFields($object) {
$viewer = $this->getViewer();
$fields = $this->buildCustomEditFields($object);
foreach ($fields as $field) {
$field
->setViewer($viewer)
->setObject($object);
}
$fields = mpull($fields, null, 'getKey');
if ($this->isEngineExtensible()) {
$extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions();
} else {
$extensions = array();
}
+ // See T13248. Create a template object to provide to extensions. We
+ // adjust the template to have the intended subtype, so that extensions
+ // may change behavior based on the form subtype.
+
+ $template_object = clone $object;
+ if ($this->getIsCreate()) {
+ if ($this->supportsSubtypes()) {
+ $config = $this->getEditEngineConfiguration();
+ $subtype = $config->getSubtype();
+ $template_object->setSubtype($subtype);
+ }
+ }
+
foreach ($extensions as $extension) {
$extension->setViewer($viewer);
- if (!$extension->supportsObject($this, $object)) {
+ if (!$extension->supportsObject($this, $template_object)) {
continue;
}
- $extension_fields = $extension->buildCustomEditFields($this, $object);
+ $extension_fields = $extension->buildCustomEditFields(
+ $this,
+ $template_object);
// TODO: Validate this in more detail with a more tailored error.
assert_instances_of($extension_fields, 'PhabricatorEditField');
foreach ($extension_fields as $field) {
$field
->setViewer($viewer)
->setObject($object);
$group_key = $field->getBulkEditGroupKey();
if ($group_key === null) {
$field->setBulkEditGroupKey('extension');
}
}
$extension_fields = mpull($extension_fields, null, 'getKey');
foreach ($extension_fields as $key => $field) {
$fields[$key] = $field;
}
}
$config = $this->getEditEngineConfiguration();
$fields = $this->willConfigureFields($object, $fields);
$fields = $config->applyConfigurationToFields($this, $object, $fields);
$fields = $this->applyPageToFields($object, $fields);
return $fields;
}
protected function willConfigureFields($object, array $fields) {
return $fields;
}
final public function supportsSubtypes() {
try {
$object = $this->newEditableObject();
} catch (Exception $ex) {
return false;
}
return ($object instanceof PhabricatorEditEngineSubtypeInterface);
}
final public function newSubtypeMap() {
return $this->newEditableObject()->newEditEngineSubtypeMap();
}
/* -( Display Text )------------------------------------------------------- */
/**
* @task text
*/
abstract public function getEngineName();
/**
* @task text
*/
abstract protected function getObjectCreateTitleText($object);
/**
* @task text
*/
protected function getFormHeaderText($object) {
$config = $this->getEditEngineConfiguration();
return $config->getName();
}
/**
* @task text
*/
abstract protected function getObjectEditTitleText($object);
/**
* @task text
*/
abstract protected function getObjectCreateShortText();
/**
* @task text
*/
abstract protected function getObjectName();
/**
* @task text
*/
abstract protected function getObjectEditShortText($object);
/**
* @task text
*/
protected function getObjectCreateButtonText($object) {
return $this->getObjectCreateTitleText($object);
}
/**
* @task text
*/
protected function getObjectEditButtonText($object) {
return pht('Save Changes');
}
/**
* @task text
*/
protected function getCommentViewSeriousHeaderText($object) {
return pht('Take Action');
}
/**
* @task text
*/
protected function getCommentViewSeriousButtonText($object) {
return pht('Submit');
}
/**
* @task text
*/
protected function getCommentViewHeaderText($object) {
return $this->getCommentViewSeriousHeaderText($object);
}
/**
* @task text
*/
protected function getCommentViewButtonText($object) {
return $this->getCommentViewSeriousButtonText($object);
}
/**
* @task text
*/
protected function getPageHeader($object) {
return null;
}
/**
* Return a human-readable header describing what this engine is used to do,
* like "Configure Maniphest Task Forms".
*
* @return string Human-readable description of the engine.
* @task text
*/
abstract public function getSummaryHeader();
/**
* Return a human-readable summary of what this engine is used to do.
*
* @return string Human-readable description of the engine.
* @task text
*/
abstract public function getSummaryText();
/* -( Edit Engine Configuration )------------------------------------------ */
protected function supportsEditEngineConfiguration() {
return true;
}
final protected function getEditEngineConfiguration() {
return $this->editEngineConfiguration;
}
public function newConfigurationQuery() {
return id(new PhabricatorEditEngineConfigurationQuery())
->setViewer($this->getViewer())
->withEngineKeys(array($this->getEngineKey()));
}
private function loadEditEngineConfigurationWithQuery(
PhabricatorEditEngineConfigurationQuery $query,
$sort_method) {
if ($sort_method) {
$results = $query->execute();
$results = msort($results, $sort_method);
$result = head($results);
} else {
$result = $query->executeOne();
}
if (!$result) {
return null;
}
$this->editEngineConfiguration = $result;
return $result;
}
private function loadEditEngineConfigurationWithIdentifier($identifier) {
$query = $this->newConfigurationQuery()
->withIdentifiers(array($identifier));
return $this->loadEditEngineConfigurationWithQuery($query, null);
}
private function loadDefaultConfiguration() {
$query = $this->newConfigurationQuery()
->withIdentifiers(
array(
self::EDITENGINECONFIG_DEFAULT,
))
->withIgnoreDatabaseConfigurations(true);
return $this->loadEditEngineConfigurationWithQuery($query, null);
}
private function loadDefaultCreateConfiguration() {
$query = $this->newConfigurationQuery()
->withIsDefault(true)
->withIsDisabled(false);
return $this->loadEditEngineConfigurationWithQuery(
$query,
'getCreateSortKey');
}
public function loadDefaultEditConfiguration($object) {
$query = $this->newConfigurationQuery()
->withIsEdit(true)
->withIsDisabled(false);
// If this object supports subtyping, we edit it with a form of the same
// subtype: so "bug" tasks get edited with "bug" forms.
if ($object instanceof PhabricatorEditEngineSubtypeInterface) {
$query->withSubtypes(
array(
$object->getEditEngineSubtype(),
));
}
return $this->loadEditEngineConfigurationWithQuery(
$query,
'getEditSortKey');
}
final public function getBuiltinEngineConfigurations() {
$configurations = $this->newBuiltinEngineConfigurations();
if (!$configurations) {
throw new Exception(
pht(
'EditEngine ("%s") returned no builtin engine configurations, but '.
'an edit engine must have at least one configuration.',
get_class($this)));
}
assert_instances_of($configurations, 'PhabricatorEditEngineConfiguration');
$has_default = false;
foreach ($configurations as $config) {
if ($config->getBuiltinKey() == self::EDITENGINECONFIG_DEFAULT) {
$has_default = true;
}
}
if (!$has_default) {
$first = head($configurations);
if (!$first->getBuiltinKey()) {
$first
->setBuiltinKey(self::EDITENGINECONFIG_DEFAULT)
->setIsDefault(true)
->setIsEdit(true);
if (!strlen($first->getName())) {
$first->setName($this->getObjectCreateShortText());
}
} else {
throw new Exception(
pht(
'EditEngine ("%s") returned builtin engine configurations, '.
'but none are marked as default and the first configuration has '.
'a different builtin key already. Mark a builtin as default or '.
'omit the key from the first configuration',
get_class($this)));
}
}
$builtins = array();
foreach ($configurations as $key => $config) {
$builtin_key = $config->getBuiltinKey();
if ($builtin_key === null) {
throw new Exception(
pht(
'EditEngine ("%s") returned builtin engine configurations, '.
'but one (with key "%s") is missing a builtin key. Provide a '.
'builtin key for each configuration (you can omit it from the '.
'first configuration in the list to automatically assign the '.
'default key).',
get_class($this),
$key));
}
if (isset($builtins[$builtin_key])) {
throw new Exception(
pht(
'EditEngine ("%s") returned builtin engine configurations, '.
'but at least two specify the same builtin key ("%s"). Engines '.
'must have unique builtin keys.',
get_class($this),
$builtin_key));
}
$builtins[$builtin_key] = $config;
}
return $builtins;
}
protected function newBuiltinEngineConfigurations() {
return array(
$this->newConfiguration(),
);
}
final protected function newConfiguration() {
return PhabricatorEditEngineConfiguration::initializeNewConfiguration(
$this->getViewer(),
$this);
}
/* -( Managing URIs )------------------------------------------------------ */
/**
* @task uri
*/
abstract protected function getObjectViewURI($object);
/**
* @task uri
*/
protected function getObjectCreateCancelURI($object) {
return $this->getApplication()->getApplicationURI();
}
/**
* @task uri
*/
protected function getEditorURI() {
return $this->getApplication()->getApplicationURI('edit/');
}
/**
* @task uri
*/
protected function getObjectEditCancelURI($object) {
return $this->getObjectViewURI($object);
}
/**
* @task uri
*/
public function getEditURI($object = null, $path = null) {
$parts = array();
$parts[] = $this->getEditorURI();
if ($object && $object->getID()) {
$parts[] = $object->getID().'/';
}
if ($path !== null) {
$parts[] = $path;
}
return implode('', $parts);
}
public function getEffectiveObjectViewURI($object) {
if ($this->getIsCreate()) {
return $this->getObjectViewURI($object);
}
$page = $this->getSelectedPage();
if ($page) {
$view_uri = $page->getViewURI();
if ($view_uri !== null) {
return $view_uri;
}
}
return $this->getObjectViewURI($object);
}
public function getEffectiveObjectEditDoneURI($object) {
return $this->getEffectiveObjectViewURI($object);
}
public function getEffectiveObjectEditCancelURI($object) {
$page = $this->getSelectedPage();
if ($page) {
$view_uri = $page->getViewURI();
if ($view_uri !== null) {
return $view_uri;
}
}
return $this->getObjectEditCancelURI($object);
}
/* -( Creating and Loading Objects )--------------------------------------- */
/**
* Initialize a new object for creation.
*
* @return object Newly initialized object.
* @task load
*/
abstract protected function newEditableObject();
/**
* Build an empty query for objects.
*
* @return PhabricatorPolicyAwareQuery Query.
* @task load
*/
abstract protected function newObjectQuery();
/**
* Test if this workflow is creating a new object or editing an existing one.
*
* @return bool True if a new object is being created.
* @task load
*/
final public function getIsCreate() {
return $this->isCreate;
}
/**
* Initialize a new object for object creation via Conduit.
*
* @return object Newly initialized object.
* @param list<wild> Raw transactions.
* @task load
*/
protected function newEditableObjectFromConduit(array $raw_xactions) {
return $this->newEditableObject();
}
/**
* Initialize a new object for documentation creation.
*
* @return object Newly initialized object.
* @task load
*/
protected function newEditableObjectForDocumentation() {
return $this->newEditableObject();
}
/**
* Flag this workflow as a create or edit.
*
* @param bool True if this is a create workflow.
* @return this
* @task load
*/
private function setIsCreate($is_create) {
$this->isCreate = $is_create;
return $this;
}
/**
* Try to load an object by ID, PHID, or monogram. This is done primarily
* to make Conduit a little easier to use.
*
* @param wild ID, PHID, or monogram.
* @param list<const> List of required capability constants, or omit for
* defaults.
* @return object Corresponding editable object.
* @task load
*/
private function newObjectFromIdentifier(
$identifier,
array $capabilities = array()) {
if (is_int($identifier) || ctype_digit($identifier)) {
$object = $this->newObjectFromID($identifier, $capabilities);
if (!$object) {
throw new Exception(
pht(
'No object exists with ID "%s".',
$identifier));
}
return $object;
}
$type_unknown = PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN;
if (phid_get_type($identifier) != $type_unknown) {
$object = $this->newObjectFromPHID($identifier, $capabilities);
if (!$object) {
throw new Exception(
pht(
'No object exists with PHID "%s".',
$identifier));
}
return $object;
}
$target = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->withNames(array($identifier))
->executeOne();
if (!$target) {
throw new Exception(
pht(
'Monogram "%s" does not identify a valid object.',
$identifier));
}
$expect = $this->newEditableObject();
$expect_class = get_class($expect);
$target_class = get_class($target);
if ($expect_class !== $target_class) {
throw new Exception(
pht(
'Monogram "%s" identifies an object of the wrong type. Loaded '.
'object has class "%s", but this editor operates on objects of '.
'type "%s".',
$identifier,
$target_class,
$expect_class));
}
// Load the object by PHID using this engine's standard query. This makes
// sure it's really valid, goes through standard policy check logic, and
// picks up any `need...()` clauses we want it to load with.
$object = $this->newObjectFromPHID($target->getPHID(), $capabilities);
if (!$object) {
throw new Exception(
pht(
'Failed to reload object identified by monogram "%s" when '.
'querying by PHID.',
$identifier));
}
return $object;
}
/**
* Load an object by ID.
*
* @param int Object ID.
* @param list<const> List of required capability constants, or omit for
* defaults.
* @return object|null Object, or null if no such object exists.
* @task load
*/
private function newObjectFromID($id, array $capabilities = array()) {
$query = $this->newObjectQuery()
->withIDs(array($id));
return $this->newObjectFromQuery($query, $capabilities);
}
/**
* Load an object by PHID.
*
* @param phid Object PHID.
* @param list<const> List of required capability constants, or omit for
* defaults.
* @return object|null Object, or null if no such object exists.
* @task load
*/
private function newObjectFromPHID($phid, array $capabilities = array()) {
$query = $this->newObjectQuery()
->withPHIDs(array($phid));
return $this->newObjectFromQuery($query, $capabilities);
}
/**
* Load an object given a configured query.
*
* @param PhabricatorPolicyAwareQuery Configured query.
* @param list<const> List of required capability constants, or omit for
* defaults.
* @return object|null Object, or null if no such object exists.
* @task load
*/
private function newObjectFromQuery(
PhabricatorPolicyAwareQuery $query,
array $capabilities = array()) {
$viewer = $this->getViewer();
if (!$capabilities) {
$capabilities = array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
$object = $query
->setViewer($viewer)
->requireCapabilities($capabilities)
->executeOne();
if (!$object) {
return null;
}
return $object;
}
/**
* Verify that an object is appropriate for editing.
*
* @param wild Loaded value.
* @return void
* @task load
*/
private function validateObject($object) {
if (!$object || !is_object($object)) {
throw new Exception(
pht(
'EditEngine "%s" created or loaded an invalid object: object must '.
'actually be an object, but is of some other type ("%s").',
get_class($this),
gettype($object)));
}
if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
throw new Exception(
pht(
'EditEngine "%s" created or loaded an invalid object: object (of '.
'class "%s") must implement "%s", but does not.',
get_class($this),
get_class($object),
'PhabricatorApplicationTransactionInterface'));
}
}
/* -( Responding to Web Requests )----------------------------------------- */
final public function buildResponse() {
$viewer = $this->getViewer();
$controller = $this->getController();
$request = $controller->getRequest();
$action = $this->getEditAction();
$capabilities = array();
$use_default = false;
$require_create = true;
switch ($action) {
case 'comment':
$capabilities = array(
PhabricatorPolicyCapability::CAN_VIEW,
);
$use_default = true;
break;
case 'parameters':
$use_default = true;
break;
case 'nodefault':
case 'nocreate':
case 'nomanage':
$require_create = false;
break;
default:
break;
}
$object = $this->getTargetObject();
if (!$object) {
$id = $request->getURIData('id');
if ($id) {
$this->setIsCreate(false);
$object = $this->newObjectFromID($id, $capabilities);
if (!$object) {
return new Aphront404Response();
}
} else {
// Make sure the viewer has permission to create new objects of
// this type if we're going to create a new object.
if ($require_create) {
$this->requireCreateCapability();
}
$this->setIsCreate(true);
$object = $this->newEditableObject();
}
} else {
$id = $object->getID();
}
$this->validateObject($object);
if ($use_default) {
$config = $this->loadDefaultConfiguration();
if (!$config) {
return new Aphront404Response();
}
} else {
$form_key = $request->getURIData('formKey');
if (strlen($form_key)) {
$config = $this->loadEditEngineConfigurationWithIdentifier($form_key);
if (!$config) {
return new Aphront404Response();
}
if ($id && !$config->getIsEdit()) {
return $this->buildNotEditFormRespose($object, $config);
}
} else {
if ($id) {
$config = $this->loadDefaultEditConfiguration($object);
if (!$config) {
return $this->buildNoEditResponse($object);
}
} else {
$config = $this->loadDefaultCreateConfiguration();
if (!$config) {
return $this->buildNoCreateResponse($object);
}
}
}
}
if ($config->getIsDisabled()) {
return $this->buildDisabledFormResponse($object, $config);
}
$page_key = $request->getURIData('pageKey');
if (!strlen($page_key)) {
$pages = $this->getPages($object);
if ($pages) {
$page_key = head_key($pages);
}
}
if (strlen($page_key)) {
$page = $this->selectPage($object, $page_key);
if (!$page) {
return new Aphront404Response();
}
}
switch ($action) {
case 'parameters':
return $this->buildParametersResponse($object);
case 'nodefault':
return $this->buildNoDefaultResponse($object);
case 'nocreate':
return $this->buildNoCreateResponse($object);
case 'nomanage':
return $this->buildNoManageResponse($object);
case 'comment':
return $this->buildCommentResponse($object);
default:
return $this->buildEditResponse($object);
}
}
private function buildCrumbs($object, $final = false) {
$controller = $this->getController();
$crumbs = $controller->buildApplicationCrumbsForEditEngine();
if ($this->getIsCreate()) {
$create_text = $this->getObjectCreateShortText();
if ($final) {
$crumbs->addTextCrumb($create_text);
} else {
$edit_uri = $this->getEditURI($object);
$crumbs->addTextCrumb($create_text, $edit_uri);
}
} else {
$crumbs->addTextCrumb(
$this->getObjectEditShortText($object),
$this->getEffectiveObjectViewURI($object));
$edit_text = pht('Edit');
if ($final) {
$crumbs->addTextCrumb($edit_text);
} else {
$edit_uri = $this->getEditURI($object);
$crumbs->addTextCrumb($edit_text, $edit_uri);
}
}
return $crumbs;
}
private function buildEditResponse($object) {
$viewer = $this->getViewer();
$controller = $this->getController();
$request = $controller->getRequest();
$fields = $this->buildEditFields($object);
$template = $object->getApplicationTransactionTemplate();
if ($this->getIsCreate()) {
$cancel_uri = $this->getObjectCreateCancelURI($object);
$submit_button = $this->getObjectCreateButtonText($object);
} else {
$cancel_uri = $this->getEffectiveObjectEditCancelURI($object);
$submit_button = $this->getObjectEditButtonText($object);
}
$config = $this->getEditEngineConfiguration()
->attachEngine($this);
// NOTE: Don't prompt users to override locks when creating objects,
// even if the default settings would create a locked object.
$can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object);
if (!$can_interact &&
!$this->getIsCreate() &&
!$request->getBool('editEngine') &&
!$request->getBool('overrideLock')) {
$lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
$dialog = $this->getController()
->newDialog()
->addHiddenInput('overrideLock', true)
->setDisableWorkflowOnSubmit(true)
->addCancelButton($cancel_uri);
return $lock->willPromptUserForLockOverrideWithDialog($dialog);
}
$validation_exception = null;
if ($request->isFormOrHisecPost() && $request->getBool('editEngine')) {
$submit_fields = $fields;
foreach ($submit_fields as $key => $field) {
if (!$field->shouldGenerateTransactionsFromSubmit()) {
unset($submit_fields[$key]);
continue;
}
}
// Before we read the submitted values, store a copy of what we would
// use if the form was empty so we can figure out which transactions are
// just setting things to their default values for the current form.
$defaults = array();
foreach ($submit_fields as $key => $field) {
$defaults[$key] = $field->getValueForTransaction();
}
foreach ($submit_fields as $key => $field) {
$field->setIsSubmittedForm(true);
if (!$field->shouldReadValueFromSubmit()) {
continue;
}
$field->readValueFromSubmit($request);
}
$xactions = array();
if ($this->getIsCreate()) {
$xactions[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_CREATE);
if ($this->supportsSubtypes()) {
$xactions[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_SUBTYPE)
->setNewValue($config->getSubtype());
}
}
foreach ($submit_fields as $key => $field) {
$field_value = $field->getValueForTransaction();
$type_xactions = $field->generateTransactions(
clone $template,
array(
'value' => $field_value,
));
foreach ($type_xactions as $type_xaction) {
$default = $defaults[$key];
if ($default === $field->getValueForTransaction()) {
$type_xaction->setIsDefaultTransaction(true);
}
$xactions[] = $type_xaction;
}
}
$editor = $object->getApplicationTransactionEditor()
->setActor($viewer)
->setContentSourceFromRequest($request)
->setCancelURI($cancel_uri)
->setContinueOnNoEffect(true);
try {
$xactions = $this->willApplyTransactions($object, $xactions);
$editor->applyTransactions($object, $xactions);
$this->didApplyTransactions($object, $xactions);
return $this->newEditResponse($request, $object, $xactions);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
foreach ($fields as $field) {
$message = $this->getValidationExceptionShortMessage($ex, $field);
if ($message === null) {
continue;
}
$field->setControlError($message);
}
}
} else {
if ($this->getIsCreate()) {
$template = $request->getStr('template');
if (strlen($template)) {
$template_object = $this->newObjectFromIdentifier(
$template,
array(
PhabricatorPolicyCapability::CAN_VIEW,
));
if (!$template_object) {
return new Aphront404Response();
}
} else {
$template_object = null;
}
if ($template_object) {
$copy_fields = $this->buildEditFields($template_object);
$copy_fields = mpull($copy_fields, null, 'getKey');
foreach ($copy_fields as $copy_key => $copy_field) {
if (!$copy_field->getIsCopyable()) {
unset($copy_fields[$copy_key]);
}
}
} else {
$copy_fields = array();
}
foreach ($fields as $field) {
if (!$field->shouldReadValueFromRequest()) {
continue;
}
$field_key = $field->getKey();
if (isset($copy_fields[$field_key])) {
$field->readValueFromField($copy_fields[$field_key]);
}
$field->readValueFromRequest($request);
}
}
}
$action_button = $this->buildEditFormActionButton($object);
if ($this->getIsCreate()) {
$header_text = $this->getFormHeaderText($object);
} else {
$header_text = $this->getObjectEditTitleText($object);
}
$show_preview = !$request->isAjax();
if ($show_preview) {
$previews = array();
foreach ($fields as $field) {
$preview = $field->getPreviewPanel();
if (!$preview) {
continue;
}
$control_id = $field->getControlID();
$preview
->setControlID($control_id)
->setPreviewURI('/transactions/remarkuppreview/');
$previews[] = $preview;
}
} else {
$previews = array();
}
$form = $this->buildEditForm($object, $fields);
$crumbs = $this->buildCrumbs($object, $final = true);
$crumbs->setBorder(true);
if ($request->isAjax()) {
return $this->getController()
->newDialog()
->setWidth(AphrontDialogView::WIDTH_FULL)
->setTitle($header_text)
->setValidationException($validation_exception)
->appendForm($form)
->addCancelButton($cancel_uri)
->addSubmitButton($submit_button);
}
$box_header = id(new PHUIHeaderView())
->setHeader($header_text);
if ($action_button) {
$box_header->addActionLink($action_button);
}
$box = id(new PHUIObjectBoxView())
->setUser($viewer)
->setHeader($box_header)
->setValidationException($validation_exception)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->appendChild($form);
// This is fairly questionable, but in use by Settings.
if ($request->getURIData('formSaved')) {
$box->setFormSaved(true);
}
$content = array(
$box,
$previews,
);
$view = new PHUITwoColumnView();
$page_header = $this->getPageHeader($object);
if ($page_header) {
$view->setHeader($page_header);
}
$view->setFooter($content);
$page = $controller->newPage()
->setTitle($header_text)
->setCrumbs($crumbs)
->appendChild($view);
$navigation = $this->getNavigation();
if ($navigation) {
$page->setNavigation($navigation);
}
return $page;
}
protected function newEditResponse(
AphrontRequest $request,
$object,
array $xactions) {
return id(new AphrontRedirectResponse())
->setURI($this->getEffectiveObjectEditDoneURI($object));
}
private function buildEditForm($object, array $fields) {
$viewer = $this->getViewer();
$controller = $this->getController();
$request = $controller->getRequest();
$fields = $this->willBuildEditForm($object, $fields);
- $request_path = $request->getRequestURI()
- ->setQueryParams(array());
+ $request_path = $request->getPath();
$form = id(new AphrontFormView())
->setUser($viewer)
->setAction($request_path)
->addHiddenInput('editEngine', 'true');
foreach ($this->contextParameters as $param) {
$form->addHiddenInput($param, $request->getStr($param));
}
$requires_mfa = false;
if ($object instanceof PhabricatorEditEngineMFAInterface) {
$mfa_engine = PhabricatorEditEngineMFAEngine::newEngineForObject($object)
->setViewer($viewer);
$requires_mfa = $mfa_engine->shouldRequireMFA();
}
if ($requires_mfa) {
$message = pht(
'You will be required to provide multi-factor credentials to make '.
'changes.');
$form->appendChild(
id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_MFA)
->setErrors(array($message)));
// TODO: This should also set workflow on the form, so the user doesn't
// lose any form data if they "Cancel". However, Maniphest currently
// overrides "newEditResponse()" if the request is Ajax and returns a
// bag of view data. This can reasonably be cleaned up when workboards
// get their next iteration.
}
foreach ($fields as $field) {
if (!$field->getIsFormField()) {
continue;
}
$field->appendToForm($form);
}
if ($this->getIsCreate()) {
$cancel_uri = $this->getObjectCreateCancelURI($object);
$submit_button = $this->getObjectCreateButtonText($object);
} else {
$cancel_uri = $this->getEffectiveObjectEditCancelURI($object);
$submit_button = $this->getObjectEditButtonText($object);
}
if (!$request->isAjax()) {
$buttons = id(new AphrontFormSubmitControl())
->setValue($submit_button);
if ($cancel_uri) {
$buttons->addCancelButton($cancel_uri);
}
$form->appendControl($buttons);
}
return $form;
}
protected function willBuildEditForm($object, array $fields) {
return $fields;
}
private function buildEditFormActionButton($object) {
if (!$this->isEngineConfigurable()) {
return null;
}
$viewer = $this->getViewer();
$action_view = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($this->buildEditFormActions($object) as $action) {
$action_view->addAction($action);
}
$action_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Configure Form'))
->setHref('#')
->setIcon('fa-gear')
->setDropdownMenu($action_view);
return $action_button;
}
private function buildEditFormActions($object) {
$actions = array();
// c4science custo (moved)
$config = $this->getEditEngineConfiguration();
$can_manage = PhabricatorPolicyFilter::hasCapability(
$this->getViewer(),
$config,
PhabricatorPolicyCapability::CAN_EDIT);
// end of c4s custo
if ($this->supportsEditEngineConfiguration()) {
$engine_key = $this->getEngineKey();
if ($can_manage) {
$manage_uri = $config->getURI();
} else {
$manage_uri = $this->getEditURI(null, 'nomanage/');
}
$view_uri = "/transactions/editengine/{$engine_key}/";
if($can_manage) { // c4science custo
$actions[] = id(new PhabricatorActionView())
->setLabel(true)
->setName(pht('Configuration'));
$actions[] = id(new PhabricatorActionView())
->setName(pht('View Form Configurations'))
->setIcon('fa-list-ul')
->setHref($view_uri);
$actions[] = id(new PhabricatorActionView())
->setName(pht('Edit Form Configuration'))
->setIcon('fa-pencil')
->setHref($manage_uri)
->setDisabled(!$can_manage)
->setWorkflow(!$can_manage);
} // end of c4s custo
}
$actions[] = id(new PhabricatorActionView())
->setLabel(true)
->setName(pht('Documentation'));
$actions[] = id(new PhabricatorActionView())
->setName(pht('Using HTTP Parameters'))
->setIcon('fa-book')
->setHref($this->getEditURI($object, 'parameters/'));
if($can_manage) { // c4science custo
$doc_href = PhabricatorEnv::getDoclink('User Guide: Customizing Forms');
$actions[] = id(new PhabricatorActionView())
->setName(pht('User Guide: Customizing Forms'))
->setIcon('fa-book')
->setHref($doc_href);
} // end of c4s custo
return $actions;
}
public function newNUXButton($text) {
$specs = $this->newCreateActionSpecifications(array());
$head = head($specs);
return id(new PHUIButtonView())
->setTag('a')
->setText($text)
->setHref($head['uri'])
->setDisabled($head['disabled'])
->setWorkflow($head['workflow'])
->setColor(PHUIButtonView::GREEN);
}
final public function addActionToCrumbs(
PHUICrumbsView $crumbs,
array $parameters = array()) {
$viewer = $this->getViewer();
$specs = $this->newCreateActionSpecifications($parameters);
$head = head($specs);
$menu_uri = $head['uri'];
$dropdown = null;
if (count($specs) > 1) {
$menu_icon = 'fa-caret-square-o-down';
$menu_name = $this->getObjectCreateShortText();
$workflow = false;
$disabled = false;
$dropdown = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($specs as $spec) {
$dropdown->addAction(
id(new PhabricatorActionView())
->setName($spec['name'])
->setIcon($spec['icon'])
->setHref($spec['uri'])
->setDisabled($head['disabled'])
->setWorkflow($head['workflow']));
}
} else {
$menu_icon = $head['icon'];
$menu_name = $head['name'];
$workflow = $head['workflow'];
$disabled = $head['disabled'];
}
$action = id(new PHUIListItemView())
->setName($menu_name)
->setHref($menu_uri)
->setIcon($menu_icon)
->setWorkflow($workflow)
->setDisabled($disabled);
if ($dropdown) {
$action->setDropdownMenu($dropdown);
}
$crumbs->addAction($action);
}
/**
* Build a raw description of available "Create New Object" UI options so
* other methods can build menus or buttons.
*/
public function newCreateActionSpecifications(array $parameters) {
$viewer = $this->getViewer();
$can_create = $this->hasCreateCapability();
if ($can_create) {
$configs = $this->loadUsableConfigurationsForCreate();
} else {
$configs = array();
}
$disabled = false;
$workflow = false;
$menu_icon = 'fa-plus-square';
$specs = array();
if (!$configs) {
if ($viewer->isLoggedIn()) {
$disabled = true;
} else {
// If the viewer isn't logged in, assume they'll get hit with a login
// dialog and are likely able to create objects after they log in.
$disabled = false;
}
$workflow = true;
if ($can_create) {
$create_uri = $this->getEditURI(null, 'nodefault/');
} else {
$create_uri = $this->getEditURI(null, 'nocreate/');
}
$specs[] = array(
'name' => $this->getObjectCreateShortText(),
'uri' => $create_uri,
'icon' => $menu_icon,
'disabled' => $disabled,
'workflow' => $workflow,
);
} else {
foreach ($configs as $config) {
$config_uri = $config->getCreateURI();
if ($parameters) {
- $config_uri = (string)id(new PhutilURI($config_uri))
- ->setQueryParams($parameters);
+ $config_uri = (string)new PhutilURI($config_uri, $parameters);
}
$specs[] = array(
'name' => $config->getDisplayName(),
'uri' => $config_uri,
'icon' => 'fa-plus',
'disabled' => false,
'workflow' => false,
);
}
}
return $specs;
}
final public function buildEditEngineCommentView($object) {
$config = $this->loadDefaultEditConfiguration($object);
if (!$config) {
// TODO: This just nukes the entire comment form if you don't have access
// to any edit forms. We might want to tailor this UX a bit.
return id(new PhabricatorApplicationTransactionCommentView())
->setNoPermission(true);
}
$viewer = $this->getViewer();
$can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object);
if (!$can_interact) {
$lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
return id(new PhabricatorApplicationTransactionCommentView())
->setEditEngineLock($lock);
}
$object_phid = $object->getPHID();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
if ($is_serious) {
$header_text = $this->getCommentViewSeriousHeaderText($object);
$button_text = $this->getCommentViewSeriousButtonText($object);
} else {
$header_text = $this->getCommentViewHeaderText($object);
$button_text = $this->getCommentViewButtonText($object);
}
$comment_uri = $this->getEditURI($object, 'comment/');
$requires_mfa = false;
if ($object instanceof PhabricatorEditEngineMFAInterface) {
$mfa_engine = PhabricatorEditEngineMFAEngine::newEngineForObject($object)
->setViewer($viewer);
$requires_mfa = $mfa_engine->shouldRequireMFA();
}
$view = id(new PhabricatorApplicationTransactionCommentView())
->setUser($viewer)
->setObjectPHID($object_phid)
->setHeaderText($header_text)
->setAction($comment_uri)
->setRequiresMFA($requires_mfa)
->setSubmitButtonName($button_text);
$draft = PhabricatorVersionedDraft::loadDraft(
$object_phid,
$viewer->getPHID());
if ($draft) {
$view->setVersionedDraft($draft);
}
$view->setCurrentVersion($this->loadDraftVersion($object));
$fields = $this->buildEditFields($object);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
$comment_actions = array();
foreach ($fields as $field) {
if (!$field->shouldGenerateTransactionsFromComment()) {
continue;
}
if (!$can_edit) {
if (!$field->getCanApplyWithoutEditCapability()) {
continue;
}
}
$comment_action = $field->getCommentAction();
if (!$comment_action) {
continue;
}
$key = $comment_action->getKey();
// TODO: Validate these better.
$comment_actions[$key] = $comment_action;
}
$comment_actions = msortv($comment_actions, 'getSortVector');
$view->setCommentActions($comment_actions);
$comment_groups = $this->newCommentActionGroups();
$view->setCommentActionGroups($comment_groups);
return $view;
}
protected function loadDraftVersion($object) {
$viewer = $this->getViewer();
if (!$viewer->isLoggedIn()) {
return null;
}
$template = $object->getApplicationTransactionTemplate();
$conn_r = $template->establishConnection('r');
// Find the most recent transaction the user has written. We'll use this
// as a version number to make sure that out-of-date drafts get discarded.
$result = queryfx_one(
$conn_r,
'SELECT id AS version FROM %T
WHERE objectPHID = %s AND authorPHID = %s
ORDER BY id DESC LIMIT 1',
$template->getTableName(),
$object->getPHID(),
$viewer->getPHID());
if ($result) {
return (int)$result['version'];
} else {
return null;
}
}
/* -( Responding to HTTP Parameter Requests )------------------------------ */
/**
* Respond to a request for documentation on HTTP parameters.
*
* @param object Editable object.
* @return AphrontResponse Response object.
* @task http
*/
private function buildParametersResponse($object) {
$controller = $this->getController();
$viewer = $this->getViewer();
$request = $controller->getRequest();
$fields = $this->buildEditFields($object);
$crumbs = $this->buildCrumbs($object);
$crumbs->addTextCrumb(pht('HTTP Parameters'));
$crumbs->setBorder(true);
$header_text = pht(
'HTTP Parameters: %s',
$this->getObjectCreateShortText());
$header = id(new PHUIHeaderView())
->setHeader($header_text);
$help_view = id(new PhabricatorApplicationEditHTTPParameterHelpView())
->setUser($viewer)
->setFields($fields);
$document = id(new PHUIDocumentView())
->setUser($viewer)
->setHeader($header)
->appendChild($help_view);
return $controller->newPage()
->setTitle(pht('HTTP Parameters'))
->setCrumbs($crumbs)
->appendChild($document);
}
private function buildError($object, $title, $body) {
$cancel_uri = $this->getObjectCreateCancelURI($object);
$dialog = $this->getController()
->newDialog()
->addCancelButton($cancel_uri);
if ($title !== null) {
$dialog->setTitle($title);
}
if ($body !== null) {
$dialog->appendParagraph($body);
}
return $dialog;
}
private function buildNoDefaultResponse($object) {
return $this->buildError(
$object,
pht('No Default Create Forms'),
pht(
'This application is not configured with any forms for creating '.
'objects that are visible to you and enabled.'));
}
private function buildNoCreateResponse($object) {
return $this->buildError(
$object,
pht('No Create Permission'),
pht('You do not have permission to create these objects.'));
}
private function buildNoManageResponse($object) {
return $this->buildError(
$object,
pht('No Manage Permission'),
pht(
'You do not have permission to configure forms for this '.
'application.'));
}
private function buildNoEditResponse($object) {
return $this->buildError(
$object,
pht('No Edit Forms'),
pht(
'You do not have access to any forms which are enabled and marked '.
'as edit forms.'));
}
private function buildNotEditFormRespose($object, $config) {
return $this->buildError(
$object,
pht('Not an Edit Form'),
pht(
'This form ("%s") is not marked as an edit form, so '.
'it can not be used to edit objects.',
$config->getName()));
}
private function buildDisabledFormResponse($object, $config) {
return $this->buildError(
$object,
pht('Form Disabled'),
pht(
'This form ("%s") has been disabled, so it can not be used.',
$config->getName()));
}
private function buildLockedObjectResponse($object) {
$dialog = $this->buildError($object, null, null);
$viewer = $this->getViewer();
$lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
return $lock->willBlockUserInteractionWithDialog($dialog);
}
private function buildCommentResponse($object) {
$viewer = $this->getViewer();
if ($this->getIsCreate()) {
return new Aphront404Response();
}
$controller = $this->getController();
$request = $controller->getRequest();
// NOTE: We handle hisec inside the transaction editor with "Sign With MFA"
// comment actions.
if (!$request->isFormOrHisecPost()) {
return new Aphront400Response();
}
$can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object);
if (!$can_interact) {
return $this->buildLockedObjectResponse($object);
}
$config = $this->loadDefaultEditConfiguration($object);
if (!$config) {
return new Aphront404Response();
}
$fields = $this->buildEditFields($object);
$is_preview = $request->isPreviewRequest();
$view_uri = $this->getEffectiveObjectViewURI($object);
$template = $object->getApplicationTransactionTemplate();
$comment_template = $template->getApplicationTransactionCommentObject();
$comment_text = $request->getStr('comment');
$actions = $request->getStr('editengine.actions');
if ($actions) {
$actions = phutil_json_decode($actions);
}
if ($is_preview) {
$version_key = PhabricatorVersionedDraft::KEY_VERSION;
$request_version = $request->getInt($version_key);
$current_version = $this->loadDraftVersion($object);
if ($request_version >= $current_version) {
$draft = PhabricatorVersionedDraft::loadOrCreateDraft(
$object->getPHID(),
$viewer->getPHID(),
$current_version);
$is_empty = (!strlen($comment_text) && !$actions);
$draft
->setProperty('comment', $comment_text)
->setProperty('actions', $actions)
->save();
$draft_engine = $this->newDraftEngine($object);
if ($draft_engine) {
$draft_engine
->setVersionedDraft($draft)
->synchronize();
}
}
}
$xactions = array();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
if ($actions) {
$action_map = array();
foreach ($actions as $action) {
$type = idx($action, 'type');
if (!$type) {
continue;
}
if (empty($fields[$type])) {
continue;
}
$action_map[$type] = $action;
}
foreach ($action_map as $type => $action) {
$field = $fields[$type];
if (!$field->shouldGenerateTransactionsFromComment()) {
continue;
}
// If you don't have edit permission on the object, you're limited in
// which actions you can take via the comment form. Most actions
// need edit permission, but some actions (like "Accept Revision")
// can be applied by anyone with view permission.
if (!$can_edit) {
if (!$field->getCanApplyWithoutEditCapability()) {
// We know the user doesn't have the capability, so this will
// raise a policy exception.
PhabricatorPolicyFilter::requireCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
}
}
if (array_key_exists('initialValue', $action)) {
$field->setInitialValue($action['initialValue']);
}
$field->readValueFromComment(idx($action, 'value'));
$type_xactions = $field->generateTransactions(
clone $template,
array(
'value' => $field->getValueForTransaction(),
));
foreach ($type_xactions as $type_xaction) {
$xactions[] = $type_xaction;
}
}
}
$auto_xactions = $this->newAutomaticCommentTransactions($object);
foreach ($auto_xactions as $xaction) {
$xactions[] = $xaction;
}
if (strlen($comment_text) || !$xactions) {
$xactions[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(clone $comment_template)
->setContent($comment_text));
}
$editor = $object->getApplicationTransactionEditor()
->setActor($viewer)
->setContinueOnNoEffect($request->isContinueRequest())
->setContinueOnMissingFields(true)
->setContentSourceFromRequest($request)
->setCancelURI($view_uri)
->setRaiseWarnings(!$request->getBool('editEngine.warnings'))
->setIsPreview($is_preview);
try {
$xactions = $editor->applyTransactions($object, $xactions);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
return id(new PhabricatorApplicationTransactionValidationResponse())
->setCancelURI($view_uri)
->setException($ex);
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
return id(new PhabricatorApplicationTransactionNoEffectResponse())
->setCancelURI($view_uri)
->setException($ex);
} catch (PhabricatorApplicationTransactionWarningException $ex) {
return id(new PhabricatorApplicationTransactionWarningResponse())
->setCancelURI($view_uri)
->setException($ex);
}
if (!$is_preview) {
PhabricatorVersionedDraft::purgeDrafts(
$object->getPHID(),
$viewer->getPHID());
$draft_engine = $this->newDraftEngine($object);
if ($draft_engine) {
$draft_engine
->setVersionedDraft(null)
->synchronize();
}
}
if ($request->isAjax() && $is_preview) {
$preview_content = $this->newCommentPreviewContent($object, $xactions);
$raw_view_data = $request->getStr('viewData');
try {
$view_data = phutil_json_decode($raw_view_data);
} catch (Exception $ex) {
$view_data = array();
}
return id(new PhabricatorApplicationTransactionResponse())
->setObject($object)
->setViewer($viewer)
->setTransactions($xactions)
->setIsPreview($is_preview)
->setViewData($view_data)
->setPreviewContent($preview_content);
} else {
return id(new AphrontRedirectResponse())
->setURI($view_uri);
}
}
protected function newDraftEngine($object) {
$viewer = $this->getViewer();
if ($object instanceof PhabricatorDraftInterface) {
$engine = $object->newDraftEngine();
} else {
$engine = new PhabricatorBuiltinDraftEngine();
}
return $engine
->setObject($object)
->setViewer($viewer);
}
/* -( Conduit )------------------------------------------------------------ */
/**
* Respond to a Conduit edit request.
*
* This method accepts a list of transactions to apply to an object, and
* either edits an existing object or creates a new one.
*
* @task conduit
*/
final public function buildConduitResponse(ConduitAPIRequest $request) {
$viewer = $this->getViewer();
$config = $this->loadDefaultConfiguration();
if (!$config) {
throw new Exception(
pht(
'Unable to load configuration for this EditEngine ("%s").',
get_class($this)));
}
$raw_xactions = $this->getRawConduitTransactions($request);
$identifier = $request->getValue('objectIdentifier');
if ($identifier) {
$this->setIsCreate(false);
// After T13186, each transaction can individually weaken or replace the
// capabilities required to apply it, so we no longer need CAN_EDIT to
// attempt to apply transactions to objects. In practice, almost all
// transactions require CAN_EDIT so we won't get very far if we don't
// have it.
$capabilities = array(
PhabricatorPolicyCapability::CAN_VIEW,
);
$object = $this->newObjectFromIdentifier(
$identifier,
$capabilities);
} else {
$this->requireCreateCapability();
$this->setIsCreate(true);
$object = $this->newEditableObjectFromConduit($raw_xactions);
}
$this->validateObject($object);
$fields = $this->buildEditFields($object);
$types = $this->getConduitEditTypesFromFields($fields);
$template = $object->getApplicationTransactionTemplate();
$xactions = $this->getConduitTransactions(
$request,
$raw_xactions,
$types,
$template);
$editor = $object->getApplicationTransactionEditor()
->setActor($viewer)
->setContentSource($request->newContentSource())
->setContinueOnNoEffect(true);
if (!$this->getIsCreate()) {
$editor->setContinueOnMissingFields(true);
}
$xactions = $editor->applyTransactions($object, $xactions);
$xactions_struct = array();
foreach ($xactions as $xaction) {
$xactions_struct[] = array(
'phid' => $xaction->getPHID(),
);
}
return array(
'object' => array(
'id' => (int)$object->getID(),
'phid' => $object->getPHID(),
),
'transactions' => $xactions_struct,
);
}
private function getRawConduitTransactions(ConduitAPIRequest $request) {
$transactions_key = 'transactions';
$xactions = $request->getValue($transactions_key);
if (!is_array($xactions)) {
throw new Exception(
pht(
'Parameter "%s" is not a list of transactions.',
$transactions_key));
}
foreach ($xactions as $key => $xaction) {
if (!is_array($xaction)) {
throw new Exception(
pht(
'Parameter "%s" must contain a list of transaction descriptions, '.
'but item with key "%s" is not a dictionary.',
$transactions_key,
$key));
}
if (!array_key_exists('type', $xaction)) {
throw new Exception(
pht(
'Parameter "%s" must contain a list of transaction descriptions, '.
'but item with key "%s" is missing a "type" field. Each '.
'transaction must have a type field.',
$transactions_key,
$key));
}
}
return $xactions;
}
/**
* Generate transactions which can be applied from edit actions in a Conduit
* request.
*
* @param ConduitAPIRequest The request.
* @param list<wild> Raw conduit transactions.
* @param list<PhabricatorEditType> Supported edit types.
* @param PhabricatorApplicationTransaction Template transaction.
* @return list<PhabricatorApplicationTransaction> Generated transactions.
* @task conduit
*/
private function getConduitTransactions(
ConduitAPIRequest $request,
array $xactions,
array $types,
PhabricatorApplicationTransaction $template) {
$viewer = $request->getUser();
$results = array();
foreach ($xactions as $key => $xaction) {
$type = $xaction['type'];
if (empty($types[$type])) {
throw new Exception(
pht(
'Transaction with key "%s" has invalid type "%s". This type is '.
'not recognized. Valid types are: %s.',
$key,
$type,
implode(', ', array_keys($types))));
}
}
if ($this->getIsCreate()) {
$results[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_CREATE);
}
$is_strict = $request->getIsStrictlyTyped();
foreach ($xactions as $xaction) {
$type = $types[$xaction['type']];
// Let the parameter type interpret the value. This allows you to
// use usernames in list<user> fields, for example.
$parameter_type = $type->getConduitParameterType();
$parameter_type->setViewer($viewer);
try {
$value = $xaction['value'];
$value = $parameter_type->getValue($xaction, 'value', $is_strict);
$value = $type->getTransactionValueFromConduit($value);
$xaction['value'] = $value;
} catch (Exception $ex) {
throw new PhutilProxyException(
pht(
'Exception when processing transaction of type "%s": %s',
$xaction['type'],
$ex->getMessage()),
$ex);
}
$type_xactions = $type->generateTransactions(
clone $template,
$xaction);
foreach ($type_xactions as $type_xaction) {
$results[] = $type_xaction;
}
}
return $results;
}
/**
* @return map<string, PhabricatorEditType>
* @task conduit
*/
private function getConduitEditTypesFromFields(array $fields) {
$types = array();
foreach ($fields as $field) {
$field_types = $field->getConduitEditTypes();
if ($field_types === null) {
continue;
}
foreach ($field_types as $field_type) {
$types[$field_type->getEditType()] = $field_type;
}
}
return $types;
}
public function getConduitEditTypes() {
$config = $this->loadDefaultConfiguration();
if (!$config) {
return array();
}
$object = $this->newEditableObjectForDocumentation();
$fields = $this->buildEditFields($object);
return $this->getConduitEditTypesFromFields($fields);
}
final public static function getAllEditEngines() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getEngineKey')
->execute();
}
final public static function getByKey(PhabricatorUser $viewer, $key) {
return id(new PhabricatorEditEngineQuery())
->setViewer($viewer)
->withEngineKeys(array($key))
->executeOne();
}
public function getIcon() {
$application = $this->getApplication();
return $application->getIcon();
}
private function loadUsableConfigurationsForCreate() {
$viewer = $this->getViewer();
$configs = id(new PhabricatorEditEngineConfigurationQuery())
->setViewer($viewer)
->withEngineKeys(array($this->getEngineKey()))
->withIsDefault(true)
->withIsDisabled(false)
->execute();
$configs = msort($configs, 'getCreateSortKey');
// Attach this specific engine to configurations we load so they can access
// any runtime configuration. For example, this allows us to generate the
// correct "Create Form" buttons when editing forms, see T12301.
foreach ($configs as $config) {
$config->attachEngine($this);
}
return $configs;
}
protected function getValidationExceptionShortMessage(
PhabricatorApplicationTransactionValidationException $ex,
PhabricatorEditField $field) {
$xaction_type = $field->getTransactionType();
if ($xaction_type === null) {
return null;
}
return $ex->getShortMessage($xaction_type);
}
protected function getCreateNewObjectPolicy() {
return PhabricatorPolicies::POLICY_USER;
}
private function requireCreateCapability() {
PhabricatorPolicyFilter::requireCapability(
$this->getViewer(),
$this,
PhabricatorPolicyCapability::CAN_EDIT);
}
private function hasCreateCapability() {
return PhabricatorPolicyFilter::hasCapability(
$this->getViewer(),
$this,
PhabricatorPolicyCapability::CAN_EDIT);
}
public function isCommentAction() {
return ($this->getEditAction() == 'comment');
}
public function getEditAction() {
$controller = $this->getController();
$request = $controller->getRequest();
return $request->getURIData('editAction');
}
protected function newCommentActionGroups() {
return array();
}
protected function newAutomaticCommentTransactions($object) {
return array();
}
protected function newCommentPreviewContent($object, array $xactions) {
return null;
}
/* -( Form Pages )--------------------------------------------------------- */
public function getSelectedPage() {
return $this->page;
}
private function selectPage($object, $page_key) {
$pages = $this->getPages($object);
if (empty($pages[$page_key])) {
return null;
}
$this->page = $pages[$page_key];
return $this->page;
}
protected function newPages($object) {
return array();
}
protected function getPages($object) {
if ($this->pages === null) {
$pages = $this->newPages($object);
assert_instances_of($pages, 'PhabricatorEditPage');
$pages = mpull($pages, null, 'getKey');
$this->pages = $pages;
}
return $this->pages;
}
private function applyPageToFields($object, array $fields) {
$pages = $this->getPages($object);
if (!$pages) {
return $fields;
}
if (!$this->getSelectedPage()) {
return $fields;
}
$page_picks = array();
$default_key = head($pages)->getKey();
foreach ($pages as $page_key => $page) {
foreach ($page->getFieldKeys() as $field_key) {
$page_picks[$field_key] = $page_key;
}
if ($page->getIsDefault()) {
$default_key = $page_key;
}
}
$page_map = array_fill_keys(array_keys($pages), array());
foreach ($fields as $field_key => $field) {
if (isset($page_picks[$field_key])) {
$page_map[$page_picks[$field_key]][$field_key] = $field;
continue;
}
// TODO: Maybe let the field pick a page to associate itself with so
// extensions can force themselves onto a particular page?
$page_map[$default_key][$field_key] = $field;
}
$page = $this->getSelectedPage();
if (!$page) {
$page = head($pages);
}
$selected_key = $page->getKey();
return $page_map[$selected_key];
}
protected function willApplyTransactions($object, array $xactions) {
return $xactions;
}
protected function didApplyTransactions($object, array $xactions) {
return;
}
/* -( Bulk Edits )--------------------------------------------------------- */
final public function newBulkEditGroupMap() {
$groups = $this->newBulkEditGroups();
$map = array();
foreach ($groups as $group) {
$key = $group->getKey();
if (isset($map[$key])) {
throw new Exception(
pht(
'Two bulk edit groups have the same key ("%s"). Each bulk edit '.
'group must have a unique key.',
$key));
}
$map[$key] = $group;
}
if ($this->isEngineExtensible()) {
$extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions();
} else {
$extensions = array();
}
foreach ($extensions as $extension) {
$extension_groups = $extension->newBulkEditGroups($this);
foreach ($extension_groups as $group) {
$key = $group->getKey();
if (isset($map[$key])) {
throw new Exception(
pht(
'Extension "%s" defines a bulk edit group with the same key '.
'("%s") as the main editor or another extension. Each bulk '.
'edit group must have a unique key.'));
}
$map[$key] = $group;
}
}
return $map;
}
protected function newBulkEditGroups() {
return array(
id(new PhabricatorBulkEditGroup())
->setKey('default')
->setLabel(pht('Primary Fields')),
id(new PhabricatorBulkEditGroup())
->setKey('extension')
->setLabel(pht('Support Applications')),
);
}
final public function newBulkEditMap() {
$config = $this->loadDefaultConfiguration();
if (!$config) {
throw new Exception(
pht('No default edit engine configuration for bulk edit.'));
}
$object = $this->newEditableObject();
$fields = $this->buildEditFields($object);
$groups = $this->newBulkEditGroupMap();
$edit_types = $this->getBulkEditTypesFromFields($fields);
$map = array();
foreach ($edit_types as $key => $type) {
$bulk_type = $type->getBulkParameterType();
if ($bulk_type === null) {
continue;
}
$bulk_label = $type->getBulkEditLabel();
if ($bulk_label === null) {
continue;
}
$group_key = $type->getBulkEditGroupKey();
if (!$group_key) {
$group_key = 'default';
}
if (!isset($groups[$group_key])) {
throw new Exception(
pht(
'Field "%s" has a bulk edit group key ("%s") with no '.
'corresponding bulk edit group.',
$key,
$group_key));
}
$map[] = array(
'label' => $bulk_label,
'xaction' => $key,
'group' => $group_key,
'control' => array(
'type' => $bulk_type->getPHUIXControlType(),
'spec' => (object)$bulk_type->getPHUIXControlSpecification(),
),
);
}
return $map;
}
final public function newRawBulkTransactions(array $xactions) {
$config = $this->loadDefaultConfiguration();
if (!$config) {
throw new Exception(
pht('No default edit engine configuration for bulk edit.'));
}
$object = $this->newEditableObject();
$fields = $this->buildEditFields($object);
$edit_types = $this->getBulkEditTypesFromFields($fields);
$template = $object->getApplicationTransactionTemplate();
$raw_xactions = array();
foreach ($xactions as $key => $xaction) {
PhutilTypeSpec::checkMap(
$xaction,
array(
'type' => 'string',
'value' => 'optional wild',
));
$type = $xaction['type'];
if (!isset($edit_types[$type])) {
throw new Exception(
pht(
'Unsupported bulk edit type "%s".',
$type));
}
$edit_type = $edit_types[$type];
// Replace the edit type with the underlying transaction type. Usually
// these are 1:1 and the transaction type just has more internal noise,
// but it's possible that this isn't the case.
$xaction['type'] = $edit_type->getTransactionType();
$value = $xaction['value'];
$value = $edit_type->getTransactionValueFromBulkEdit($value);
$xaction['value'] = $value;
$xaction_objects = $edit_type->generateTransactions(
clone $template,
$xaction);
foreach ($xaction_objects as $xaction_object) {
$raw_xaction = array(
'type' => $xaction_object->getTransactionType(),
'metadata' => $xaction_object->getMetadata(),
'new' => $xaction_object->getNewValue(),
);
if ($xaction_object->hasOldValue()) {
$raw_xaction['old'] = $xaction_object->getOldValue();
}
if ($xaction_object->hasComment()) {
$comment = $xaction_object->getComment();
$raw_xaction['comment'] = $comment->getContent();
}
$raw_xactions[] = $raw_xaction;
}
}
return $raw_xactions;
}
private function getBulkEditTypesFromFields(array $fields) {
$types = array();
foreach ($fields as $field) {
$field_types = $field->getBulkEditTypes();
if ($field_types === null) {
continue;
}
foreach ($field_types as $field_type) {
$types[$field_type->getEditType()] = $field_type;
}
}
return $types;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getPHID() {
return get_class($this);
}
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getCreateNewObjectPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
}
diff --git a/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php b/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php
index 9e754a3ca..6e1d1de11 100644
--- a/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php
+++ b/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php
@@ -1,247 +1,281 @@
<?php
final class PhabricatorEditEngineSubtype
extends Phobject {
const SUBTYPE_DEFAULT = 'default';
private $key;
private $name;
private $icon;
private $tagText;
private $color;
private $childSubtypes = array();
private $childIdentifiers = array();
+ private $fieldConfiguration = array();
public function setKey($key) {
$this->key = $key;
return $this;
}
public function getKey() {
return $this->key;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setIcon($icon) {
$this->icon = $icon;
return $this;
}
public function getIcon() {
return $this->icon;
}
public function setTagText($text) {
$this->tagText = $text;
return $this;
}
public function getTagText() {
return $this->tagText;
}
public function setColor($color) {
$this->color = $color;
return $this;
}
public function getColor() {
return $this->color;
}
public function setChildSubtypes(array $child_subtypes) {
$this->childSubtypes = $child_subtypes;
return $this;
}
public function getChildSubtypes() {
return $this->childSubtypes;
}
public function setChildFormIdentifiers(array $child_identifiers) {
$this->childIdentifiers = $child_identifiers;
return $this;
}
public function getChildFormIdentifiers() {
return $this->childIdentifiers;
}
public function hasTagView() {
return (bool)strlen($this->getTagText());
}
public function newTagView() {
$view = id(new PHUITagView())
->setType(PHUITagView::TYPE_OUTLINE)
->setName($this->getTagText());
$color = $this->getColor();
if ($color) {
$view->setColor($color);
}
return $view;
}
+ public function setSubtypeFieldConfiguration(
+ $subtype_key,
+ array $configuration) {
+ $this->fieldConfiguration[$subtype_key] = $configuration;
+ return $this;
+ }
+
+ public function getSubtypeFieldConfiguration($subtype_key) {
+ return idx($this->fieldConfiguration, $subtype_key);
+ }
+
public static function validateSubtypeKey($subtype) {
if (strlen($subtype) > 64) {
throw new Exception(
pht(
'Subtype "%s" is not valid: subtype keys must be no longer than '.
'64 bytes.',
$subtype));
}
if (strlen($subtype) < 3) {
throw new Exception(
pht(
'Subtype "%s" is not valid: subtype keys must have a minimum '.
'length of 3 bytes.',
$subtype));
}
if (!preg_match('/^[a-z]+\z/', $subtype)) {
throw new Exception(
pht(
'Subtype "%s" is not valid: subtype keys may only contain '.
'lowercase latin letters ("a" through "z").',
$subtype));
}
}
public static function validateConfiguration($config) {
if (!is_array($config)) {
throw new Exception(
pht(
'Subtype configuration is invalid: it must be a list of subtype '.
'specifications.'));
}
$map = array();
foreach ($config as $value) {
PhutilTypeSpec::checkMap(
$value,
array(
'key' => 'string',
'name' => 'string',
'tag' => 'optional string',
'color' => 'optional string',
'icon' => 'optional string',
'children' => 'optional map<string, wild>',
+ 'fields' => 'optional map<string, wild>',
));
$key = $value['key'];
self::validateSubtypeKey($key);
if (isset($map[$key])) {
throw new Exception(
pht(
'Subtype configuration is invalid: two subtypes use the same '.
'key ("%s"). Each subtype must have a unique key.',
$key));
}
$map[$key] = true;
$name = $value['name'];
if (!strlen($name)) {
throw new Exception(
pht(
'Subtype configuration is invalid: subtype with key "%s" has '.
'no name. Subtypes must have a name.',
$key));
}
$children = idx($value, 'children');
if ($children) {
PhutilTypeSpec::checkMap(
$children,
array(
'subtypes' => 'optional list<string>',
'forms' => 'optional list<string|int>',
));
$child_subtypes = idx($children, 'subtypes');
$child_forms = idx($children, 'forms');
if ($child_subtypes && $child_forms) {
throw new Exception(
pht(
'Subtype configuration is invalid: subtype with key "%s" '.
'specifies both child subtypes and child forms. Specify one '.
'or the other, but not both.'));
}
}
+
+ $fields = idx($value, 'fields');
+ if ($fields) {
+ foreach ($fields as $field_key => $configuration) {
+ PhutilTypeSpec::checkMap(
+ $configuration,
+ array(
+ 'disabled' => 'optional bool',
+ 'name' => 'optional string',
+ ));
+ }
+ }
}
if (!isset($map[self::SUBTYPE_DEFAULT])) {
throw new Exception(
pht(
'Subtype configuration is invalid: there is no subtype defined '.
'with key "%s". This subtype is required and must be defined.',
self::SUBTYPE_DEFAULT));
}
}
public static function newSubtypeMap(array $config) {
$map = array();
foreach ($config as $entry) {
$key = $entry['key'];
$name = $entry['name'];
$tag_text = idx($entry, 'tag');
if ($tag_text === null) {
if ($key != self::SUBTYPE_DEFAULT) {
$tag_text = phutil_utf8_strtoupper($name);
}
}
$color = idx($entry, 'color', 'blue');
$icon = idx($entry, 'icon', 'fa-drivers-license-o');
$subtype = id(new self())
->setKey($key)
->setName($name)
->setTagText($tag_text)
->setIcon($icon);
if ($color) {
$subtype->setColor($color);
}
$children = idx($entry, 'children', array());
$child_subtypes = idx($children, 'subtypes');
$child_forms = idx($children, 'forms');
if ($child_subtypes) {
$subtype->setChildSubtypes($child_subtypes);
}
if ($child_forms) {
$subtype->setChildFormIdentifiers($child_forms);
}
+ $field_configurations = idx($entry, 'fields');
+ if ($field_configurations) {
+ foreach ($field_configurations as $field_key => $field_configuration) {
+ $subtype->setSubtypeFieldConfiguration(
+ $field_key,
+ $field_configuration);
+ }
+ }
+
$map[$key] = $subtype;
}
return new PhabricatorEditEngineSubtypeMap($map);
}
public function newIconView() {
return id(new PHUIIconView())
->setIcon($this->getIcon(), $this->getColor());
}
}
diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php
index f9db0e238..d963ea2ec 100644
--- a/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php
+++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php
@@ -1,164 +1,285 @@
<?php
final class PhabricatorApplicationTransactionCommentEditor
extends PhabricatorEditor {
private $contentSource;
private $actingAsPHID;
+ private $request;
+ private $cancelURI;
+ private $isNewComment;
public function setActingAsPHID($acting_as_phid) {
$this->actingAsPHID = $acting_as_phid;
return $this;
}
public function getActingAsPHID() {
if ($this->actingAsPHID) {
return $this->actingAsPHID;
}
return $this->getActor()->getPHID();
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function getContentSource() {
return $this->contentSource;
}
+ public function setRequest(AphrontRequest $request) {
+ $this->request = $request;
+ return $this;
+ }
+
+ public function getRequest() {
+ return $this->request;
+ }
+
+ public function setCancelURI($cancel_uri) {
+ $this->cancelURI = $cancel_uri;
+ return $this;
+ }
+
+ public function getCancelURI() {
+ return $this->cancelURI;
+ }
+
+ public function setIsNewComment($is_new) {
+ $this->isNewComment = $is_new;
+ return $this;
+ }
+
+ public function getIsNewComment() {
+ return $this->isNewComment;
+ }
+
/**
* Edit a transaction's comment. This method effects the required create,
* update or delete to set the transaction's comment to the provided comment.
*/
public function applyEdit(
PhabricatorApplicationTransaction $xaction,
PhabricatorApplicationTransactionComment $comment) {
$this->validateEdit($xaction, $comment);
$actor = $this->requireActor();
+ $this->applyMFAChecks($xaction, $comment);
+
$comment->setContentSource($this->getContentSource());
$comment->setAuthorPHID($this->getActingAsPHID());
// TODO: This needs to be more sophisticated once we have meta-policies.
$comment->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC);
$comment->setEditPolicy($this->getActingAsPHID());
$file_phids = PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles(
$actor,
array(
$comment->getContent(),
));
$xaction->openTransaction();
$xaction->beginReadLocking();
if ($xaction->getID()) {
$xaction->reload();
}
$new_version = $xaction->getCommentVersion() + 1;
$comment->setCommentVersion($new_version);
$comment->setTransactionPHID($xaction->getPHID());
$comment->save();
$old_comment = $xaction->getComment();
$comment->attachOldComment($old_comment);
$xaction->setCommentVersion($new_version);
$xaction->setCommentPHID($comment->getPHID());
$xaction->setViewPolicy($comment->getViewPolicy());
$xaction->setEditPolicy($comment->getEditPolicy());
$xaction->save();
$xaction->attachComment($comment);
// For comment edits, we need to make sure there are no automagical
// transactions like adding mentions or projects.
if ($new_version > 1) {
$object = id(new PhabricatorObjectQuery())
->withPHIDs(array($xaction->getObjectPHID()))
->setViewer($this->getActor())
->executeOne();
if ($object &&
$object instanceof PhabricatorApplicationTransactionInterface) {
$editor = $object->getApplicationTransactionEditor();
$editor->setActor($this->getActor());
$support_xactions = $editor->getExpandedSupportTransactions(
$object,
$xaction);
if ($support_xactions) {
$editor
->setContentSource($this->getContentSource())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($object, $support_xactions);
}
}
}
$xaction->endReadLocking();
$xaction->saveTransaction();
// Add links to any files newly referenced by the edit.
if ($file_phids) {
$editor = new PhabricatorEdgeEditor();
foreach ($file_phids as $file_phid) {
$editor->addEdge(
$xaction->getObjectPHID(),
PhabricatorObjectHasFileEdgeType::EDGECONST ,
$file_phid);
}
$editor->save();
}
return $this;
}
/**
* Validate that the edit is permissible, and the actor has permission to
* perform it.
*/
private function validateEdit(
PhabricatorApplicationTransaction $xaction,
PhabricatorApplicationTransactionComment $comment) {
if (!$xaction->getPHID()) {
throw new Exception(
pht(
'Transaction must have a PHID before calling %s!',
'applyEdit()'));
}
$type_comment = PhabricatorTransactions::TYPE_COMMENT;
if ($xaction->getTransactionType() == $type_comment) {
if ($comment->getPHID()) {
throw new Exception(
pht('Transaction comment must not yet have a PHID!'));
}
}
if (!$this->getContentSource()) {
throw new PhutilInvalidStateException('applyEdit');
}
$actor = $this->requireActor();
PhabricatorPolicyFilter::requireCapability(
$actor,
$xaction,
PhabricatorPolicyCapability::CAN_VIEW);
if ($comment->getIsRemoved() && $actor->getIsAdmin()) {
// NOTE: Administrators can remove comments by any user, and don't need
// to pass the edit check.
} else {
PhabricatorPolicyFilter::requireCapability(
$actor,
$xaction,
PhabricatorPolicyCapability::CAN_EDIT);
}
}
+ private function applyMFAChecks(
+ PhabricatorApplicationTransaction $xaction,
+ PhabricatorApplicationTransactionComment $comment) {
+ $actor = $this->requireActor();
+
+ // We don't do any MFA checks here when you're creating a comment for the
+ // first time (the parent editor handles them for us), so we can just bail
+ // out if this is the creation flow.
+ if ($this->getIsNewComment()) {
+ return;
+ }
+
+ $request = $this->getRequest();
+ if (!$request) {
+ throw new PhutilInvalidStateException('setRequest');
+ }
+
+ $cancel_uri = $this->getCancelURI();
+ if (!strlen($cancel_uri)) {
+ throw new PhutilInvalidStateException('setCancelURI');
+ }
+
+ // If you're deleting a comment, we try to prompt you for MFA if you have
+ // it configured, but do not require that you have it configured. In most
+ // cases, this is administrators removing content.
+
+ // See PHI1173. If you're editing a comment you authored and the original
+ // comment was signed with MFA, you MUST have MFA on your account and you
+ // MUST sign the edit with MFA. Otherwise, we can end up with an MFA badge
+ // on different content than what was signed.
+
+ $want_mfa = false;
+ $need_mfa = false;
+
+ if ($comment->getIsRemoved()) {
+ // Try to prompt on removal.
+ $want_mfa = true;
+ }
+
+ if ($xaction->getIsMFATransaction()) {
+ if ($actor->getPHID() === $xaction->getAuthorPHID()) {
+ // Strictly require MFA if the original transaction was signed and
+ // you're the author.
+ $want_mfa = true;
+ $need_mfa = true;
+ }
+ }
+
+ if (!$want_mfa) {
+ return;
+ }
+
+ if ($need_mfa) {
+ $factors = id(new PhabricatorAuthFactorConfigQuery())
+ ->setViewer($actor)
+ ->withUserPHIDs(array($this->getActingAsPHID()))
+ ->withFactorProviderStatuses(
+ array(
+ PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
+ PhabricatorAuthFactorProviderStatus::STATUS_DEPRECATED,
+ ))
+ ->execute();
+ if (!$factors) {
+ $error = new PhabricatorApplicationTransactionValidationError(
+ $xaction->getTransactionType(),
+ pht('No MFA'),
+ pht(
+ 'This comment was signed with MFA, so edits to it must also be '.
+ 'signed with MFA. You do not have any MFA factors attached to '.
+ 'your account, so you can not sign this edit. Add MFA to your '.
+ 'account in Settings.'),
+ $xaction);
+
+ throw new PhabricatorApplicationTransactionValidationException(
+ array(
+ $error,
+ ));
+ }
+ }
+
+ $workflow_key = sprintf(
+ 'comment.edit(%s, %d)',
+ $xaction->getPHID(),
+ $xaction->getComment()->getID());
+
+ $hisec_token = id(new PhabricatorAuthSessionEngine())
+ ->setWorkflowKey($workflow_key)
+ ->requireHighSecurityToken($actor, $request, $cancel_uri);
+ }
}
diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
index 91825eb73..06c9b4321 100644
--- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
+++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
@@ -1,5099 +1,5248 @@
<?php
/**
*
* Publishing and Managing State
* ======
*
* After applying changes, the Editor queues a worker to publish mail, feed,
* and notifications, and to perform other background work like updating search
* indexes. This allows it to do this work without impacting performance for
* users.
*
* When work is moved to the daemons, the Editor state is serialized by
* @{method:getWorkerState}, then reloaded in a daemon process by
* @{method:loadWorkerState}. **This is fragile.**
*
* State is not persisted into the daemons by default, because we can not send
* arbitrary objects into the queue. This means the default behavior of any
* state properties is to reset to their defaults without warning prior to
* publishing.
*
* The easiest way to avoid this is to keep Editors stateless: the overwhelming
* majority of Editors can be written statelessly. If you need to maintain
* state, you can either:
*
* - not require state to exist during publishing; or
* - pass state to the daemons by implementing @{method:getCustomWorkerState}
* and @{method:loadCustomWorkerState}.
*
* This architecture isn't ideal, and we may eventually split this class into
* "Editor" and "Publisher" parts to make it more robust. See T6367 for some
* discussion and context.
*
* @task mail Sending Mail
* @task feed Publishing Feed Stories
* @task search Search Index
* @task files Integration with Files
* @task workers Managing Workers
*/
abstract class PhabricatorApplicationTransactionEditor
extends PhabricatorEditor {
private $contentSource;
private $object;
private $xactions;
private $isNewObject;
private $mentionedPHIDs;
private $continueOnNoEffect;
private $continueOnMissingFields;
private $raiseWarnings;
private $parentMessageID;
private $heraldAdapter;
private $heraldTranscript;
private $subscribers;
private $unmentionablePHIDMap = array();
private $applicationEmail;
private $isPreview;
private $isHeraldEditor;
private $isInverseEdgeEditor;
private $actingAsPHID;
private $heraldEmailPHIDs = array();
private $heraldForcedEmailPHIDs = array();
private $heraldHeader;
private $mailToPHIDs = array();
private $mailCCPHIDs = array();
private $feedNotifyPHIDs = array();
private $feedRelatedPHIDs = array();
private $feedShouldPublish = false;
private $mailShouldSend = false;
private $modularTypes;
private $silent;
- private $mustEncrypt;
+ private $mustEncrypt = array();
private $stampTemplates = array();
private $mailStamps = array();
private $oldTo = array();
private $oldCC = array();
private $mailRemovedPHIDs = array();
private $mailUnexpandablePHIDs = array();
private $mailMutedPHIDs = array();
private $webhookMap = array();
private $transactionQueue = array();
private $sendHistory = false;
private $shouldRequireMFA = false;
private $hasRequiredMFA = false;
private $request;
private $cancelURI;
private $extensions;
+ private $parentEditor;
+ private $subEditors = array();
+ private $publishableObject;
+ private $publishableTransactions;
+
const STORAGE_ENCODING_BINARY = 'binary';
/**
* Get the class name for the application this editor is a part of.
*
* Uninstalling the application will disable the editor.
*
* @return string Editor's application class name.
*/
abstract public function getEditorApplicationClass();
/**
* Get a description of the objects this editor edits, like "Differential
* Revisions".
*
* @return string Human readable description of edited objects.
*/
abstract public function getEditorObjectsDescription();
public function setActingAsPHID($acting_as_phid) {
$this->actingAsPHID = $acting_as_phid;
return $this;
}
public function getActingAsPHID() {
if ($this->actingAsPHID) {
return $this->actingAsPHID;
}
return $this->getActor()->getPHID();
}
/**
* When the editor tries to apply transactions that have no effect, should
* it raise an exception (default) or drop them and continue?
*
* Generally, you will set this flag for edits coming from "Edit" interfaces,
* and leave it cleared for edits coming from "Comment" interfaces, so the
* user will get a useful error if they try to submit a comment that does
* nothing (e.g., empty comment with a status change that has already been
* performed by another user).
*
* @param bool True to drop transactions without effect and continue.
* @return this
*/
public function setContinueOnNoEffect($continue) {
$this->continueOnNoEffect = $continue;
return $this;
}
public function getContinueOnNoEffect() {
return $this->continueOnNoEffect;
}
/**
* When the editor tries to apply transactions which don't populate all of
* an object's required fields, should it raise an exception (default) or
* drop them and continue?
*
* For example, if a user adds a new required custom field (like "Severity")
* to a task, all existing tasks won't have it populated. When users
* manually edit existing tasks, it's usually desirable to have them provide
* a severity. However, other operations (like batch editing just the
* owner of a task) will fail by default.
*
* By setting this flag for edit operations which apply to specific fields
* (like the priority, batch, and merge editors in Maniphest), these
* operations can continue to function even if an object is outdated.
*
* @param bool True to continue when transactions don't completely satisfy
* all required fields.
* @return this
*/
public function setContinueOnMissingFields($continue_on_missing_fields) {
$this->continueOnMissingFields = $continue_on_missing_fields;
return $this;
}
public function getContinueOnMissingFields() {
return $this->continueOnMissingFields;
}
/**
* Not strictly necessary, but reply handlers ideally set this value to
* make email threading work better.
*/
public function setParentMessageID($parent_message_id) {
$this->parentMessageID = $parent_message_id;
return $this;
}
public function getParentMessageID() {
return $this->parentMessageID;
}
public function getIsNewObject() {
return $this->isNewObject;
}
public function getMentionedPHIDs() {
return $this->mentionedPHIDs;
}
public function setIsPreview($is_preview) {
$this->isPreview = $is_preview;
return $this;
}
public function getIsPreview() {
return $this->isPreview;
}
public function setIsSilent($silent) {
$this->silent = $silent;
return $this;
}
public function getIsSilent() {
return $this->silent;
}
public function getMustEncrypt() {
return $this->mustEncrypt;
}
public function getHeraldRuleMonograms() {
// Convert the stored "<123>, <456>" string into a list: "H123", "H456".
$list = $this->heraldHeader;
$list = preg_split('/[, ]+/', $list);
foreach ($list as $key => $item) {
$item = trim($item, '<>');
if (!is_numeric($item)) {
unset($list[$key]);
continue;
}
$list[$key] = 'H'.$item;
}
return $list;
}
public function setIsInverseEdgeEditor($is_inverse_edge_editor) {
$this->isInverseEdgeEditor = $is_inverse_edge_editor;
return $this;
}
public function getIsInverseEdgeEditor() {
return $this->isInverseEdgeEditor;
}
public function setIsHeraldEditor($is_herald_editor) {
$this->isHeraldEditor = $is_herald_editor;
return $this;
}
public function getIsHeraldEditor() {
return $this->isHeraldEditor;
}
public function setUnmentionablePHIDMap(array $map) {
$this->unmentionablePHIDMap = $map;
return $this;
}
public function getUnmentionablePHIDMap() {
return $this->unmentionablePHIDMap;
}
protected function shouldEnableMentions(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
public function setApplicationEmail(
PhabricatorMetaMTAApplicationEmail $email) {
$this->applicationEmail = $email;
return $this;
}
public function getApplicationEmail() {
return $this->applicationEmail;
}
public function setRaiseWarnings($raise_warnings) {
$this->raiseWarnings = $raise_warnings;
return $this;
}
public function getRaiseWarnings() {
return $this->raiseWarnings;
}
public function setShouldRequireMFA($should_require_mfa) {
if ($this->hasRequiredMFA) {
throw new Exception(
pht(
'Call to setShouldRequireMFA() is too late: this Editor has already '.
'checked for MFA requirements.'));
}
$this->shouldRequireMFA = $should_require_mfa;
return $this;
}
public function getShouldRequireMFA() {
return $this->shouldRequireMFA;
}
public function getTransactionTypesForObject($object) {
$old = $this->object;
try {
$this->object = $object;
$result = $this->getTransactionTypes();
$this->object = $old;
} catch (Exception $ex) {
$this->object = $old;
throw $ex;
}
return $result;
}
public function getTransactionTypes() {
$types = array();
$types[] = PhabricatorTransactions::TYPE_CREATE;
$types[] = PhabricatorTransactions::TYPE_HISTORY;
if ($this->object instanceof PhabricatorEditEngineSubtypeInterface) {
$types[] = PhabricatorTransactions::TYPE_SUBTYPE;
}
if ($this->object instanceof PhabricatorSubscribableInterface) {
$types[] = PhabricatorTransactions::TYPE_SUBSCRIBERS;
}
if ($this->object instanceof PhabricatorCustomFieldInterface) {
$types[] = PhabricatorTransactions::TYPE_CUSTOMFIELD;
}
if ($this->object instanceof PhabricatorTokenReceiverInterface) {
$types[] = PhabricatorTransactions::TYPE_TOKEN;
}
if ($this->object instanceof PhabricatorProjectInterface ||
$this->object instanceof PhabricatorMentionableInterface) {
$types[] = PhabricatorTransactions::TYPE_EDGE;
}
if ($this->object instanceof PhabricatorSpacesInterface) {
$types[] = PhabricatorTransactions::TYPE_SPACE;
}
$types[] = PhabricatorTransactions::TYPE_MFA;
$template = $this->object->getApplicationTransactionTemplate();
if ($template instanceof PhabricatorModularTransaction) {
$xtypes = $template->newModularTransactionTypes();
foreach ($xtypes as $xtype) {
$types[] = $xtype->getTransactionTypeConstant();
}
}
if ($template) {
- try {
- $comment = $template->getApplicationTransactionCommentObject();
- } catch (PhutilMethodNotImplementedException $ex) {
- $comment = null;
- }
-
+ $comment = $template->getApplicationTransactionCommentObject();
if ($comment) {
$types[] = PhabricatorTransactions::TYPE_COMMENT;
}
}
return $types;
}
private function adjustTransactionValues(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
if ($xaction->shouldGenerateOldValue()) {
$old = $this->getTransactionOldValue($object, $xaction);
$xaction->setOldValue($old);
}
$new = $this->getTransactionNewValue($object, $xaction);
$xaction->setNewValue($new);
}
private function getTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
return $xtype->generateOldValue($object);
}
switch ($type) {
case PhabricatorTransactions::TYPE_CREATE:
case PhabricatorTransactions::TYPE_HISTORY:
return null;
case PhabricatorTransactions::TYPE_SUBTYPE:
return $object->getEditEngineSubtype();
case PhabricatorTransactions::TYPE_MFA:
return null;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return array_values($this->subscribers);
case PhabricatorTransactions::TYPE_VIEW_POLICY:
if ($this->getIsNewObject()) {
return null;
}
return $object->getViewPolicy();
case PhabricatorTransactions::TYPE_EDIT_POLICY:
if ($this->getIsNewObject()) {
return null;
}
return $object->getEditPolicy();
case PhabricatorTransactions::TYPE_JOIN_POLICY:
if ($this->getIsNewObject()) {
return null;
}
return $object->getJoinPolicy();
case PhabricatorTransactions::TYPE_SPACE:
if ($this->getIsNewObject()) {
return null;
}
$space_phid = $object->getSpacePHID();
if ($space_phid === null) {
$default_space = PhabricatorSpacesNamespaceQuery::getDefaultSpace();
if ($default_space) {
$space_phid = $default_space->getPHID();
}
}
return $space_phid;
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $xaction->getMetadataValue('edge:type');
if (!$edge_type) {
throw new Exception(
pht(
"Edge transaction has no '%s'!",
'edge:type'));
}
// See T13082. If this is an inverse edit, the parent editor has
// already populated the transaction values correctly.
if ($this->getIsInverseEdgeEditor()) {
return $xaction->getOldValue();
}
$old_edges = array();
if ($object->getPHID()) {
$edge_src = $object->getPHID();
$old_edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($edge_src))
->withEdgeTypes(array($edge_type))
->needEdgeData(true)
->execute();
$old_edges = $old_edges[$edge_src][$edge_type];
}
return $old_edges;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
// NOTE: Custom fields have their old value pre-populated when they are
// built by PhabricatorCustomFieldList.
return $xaction->getOldValue();
case PhabricatorTransactions::TYPE_COMMENT:
return null;
default:
return $this->getCustomTransactionOldValue($object, $xaction);
}
}
private function getTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
return $xtype->generateNewValue($object, $xaction->getNewValue());
}
switch ($type) {
case PhabricatorTransactions::TYPE_CREATE:
return null;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return $this->getPHIDTransactionNewValue($xaction);
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_INLINESTATE:
case PhabricatorTransactions::TYPE_SUBTYPE:
case PhabricatorTransactions::TYPE_HISTORY:
return $xaction->getNewValue();
case PhabricatorTransactions::TYPE_MFA:
return true;
case PhabricatorTransactions::TYPE_SPACE:
$space_phid = $xaction->getNewValue();
if (!strlen($space_phid)) {
// If an install has no Spaces or the Spaces controls are not visible
// to the viewer, we might end up with the empty string here instead
// of a strict `null`, because some controller just used `getStr()`
// to read the space PHID from the request.
// Just make this work like callers might reasonably expect so we
// don't need to handle this specially in every EditController.
return $this->getActor()->getDefaultSpacePHID();
} else {
return $space_phid;
}
case PhabricatorTransactions::TYPE_EDGE:
// See T13082. If this is an inverse edit, the parent editor has
// already populated appropriate transaction values.
if ($this->getIsInverseEdgeEditor()) {
return $xaction->getNewValue();
}
$new_value = $this->getEdgeTransactionNewValue($xaction);
$edge_type = $xaction->getMetadataValue('edge:type');
$type_project = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
if ($edge_type == $type_project) {
$new_value = $this->applyProjectConflictRules($new_value);
}
return $new_value;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->getNewValueFromApplicationTransactions($xaction);
case PhabricatorTransactions::TYPE_COMMENT:
return null;
default:
return $this->getCustomTransactionNewValue($object, $xaction);
}
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
throw new Exception(pht('Capability not supported!'));
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
throw new Exception(pht('Capability not supported!'));
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_CREATE:
case PhabricatorTransactions::TYPE_HISTORY:
return true;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->getApplicationTransactionHasEffect($xaction);
case PhabricatorTransactions::TYPE_EDGE:
// A straight value comparison here doesn't always get the right
// result, because newly added edges aren't fully populated. Instead,
// compare the changes in a more granular way.
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$old_dst = array_keys($old);
$new_dst = array_keys($new);
// NOTE: For now, we don't consider edge reordering to be a change.
// We have very few order-dependent edges and effectively no order
// oriented UI. This might change in the future.
sort($old_dst);
sort($new_dst);
if ($old_dst !== $new_dst) {
// We've added or removed edges, so this transaction definitely
// has an effect.
return true;
}
// We haven't added or removed edges, but we might have changed
// edge data.
foreach ($old as $key => $old_value) {
$new_value = $new[$key];
if ($old_value['data'] !== $new_value['data']) {
return true;
}
}
return false;
}
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
return $xtype->getTransactionHasEffect(
$object,
$xaction->getOldValue(),
$xaction->getNewValue());
}
if ($xaction->hasComment()) {
return true;
}
return ($xaction->getOldValue() !== $xaction->getNewValue());
}
protected function shouldApplyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
protected function applyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
throw new PhutilMethodNotImplementedException();
}
private function applyInternalEffects(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
return $xtype->applyInternalEffects($object, $xaction->getNewValue());
}
switch ($type) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->applyApplicationTransactionInternalEffects($xaction);
case PhabricatorTransactions::TYPE_CREATE:
case PhabricatorTransactions::TYPE_HISTORY:
case PhabricatorTransactions::TYPE_SUBTYPE:
case PhabricatorTransactions::TYPE_MFA:
case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
case PhabricatorTransactions::TYPE_INLINESTATE:
case PhabricatorTransactions::TYPE_EDGE:
case PhabricatorTransactions::TYPE_SPACE:
case PhabricatorTransactions::TYPE_COMMENT:
return $this->applyBuiltinInternalTransaction($object, $xaction);
}
return $this->applyCustomInternalTransaction($object, $xaction);
}
private function applyExternalEffects(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
return $xtype->applyExternalEffects($object, $xaction->getNewValue());
}
switch ($type) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$subeditor = id(new PhabricatorSubscriptionsEditor())
->setObject($object)
->setActor($this->requireActor());
$old_map = array_fuse($xaction->getOldValue());
$new_map = array_fuse($xaction->getNewValue());
$subeditor->unsubscribe(
array_keys(
array_diff_key($old_map, $new_map)));
$subeditor->subscribeExplicit(
array_keys(
array_diff_key($new_map, $old_map)));
$subeditor->save();
// for the rest of these edits, subscribers should include those just
// added as well as those just removed.
$subscribers = array_unique(array_merge(
$this->subscribers,
$xaction->getOldValue(),
$xaction->getNewValue()));
$this->subscribers = $subscribers;
return $this->applyBuiltinExternalTransaction($object, $xaction);
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->applyApplicationTransactionExternalEffects($xaction);
case PhabricatorTransactions::TYPE_CREATE:
case PhabricatorTransactions::TYPE_HISTORY:
case PhabricatorTransactions::TYPE_SUBTYPE:
case PhabricatorTransactions::TYPE_MFA:
case PhabricatorTransactions::TYPE_EDGE:
case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
case PhabricatorTransactions::TYPE_INLINESTATE:
case PhabricatorTransactions::TYPE_SPACE:
case PhabricatorTransactions::TYPE_COMMENT:
return $this->applyBuiltinExternalTransaction($object, $xaction);
}
return $this->applyCustomExternalTransaction($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
throw new Exception(
pht(
"Transaction type '%s' is missing an internal apply implementation!",
$type));
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
throw new Exception(
pht(
"Transaction type '%s' is missing an external apply implementation!",
$type));
}
/**
* @{class:PhabricatorTransactions} provides many built-in transactions
* which should not require much - if any - code in specific applications.
*
* This method is a hook for the exceedingly-rare cases where you may need
* to do **additional** work for built-in transactions. Developers should
* extend this method, making sure to return the parent implementation
* regardless of handling any transactions.
*
* See also @{method:applyBuiltinExternalTransaction}.
*/
protected function applyBuiltinInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_VIEW_POLICY:
$object->setViewPolicy($xaction->getNewValue());
break;
case PhabricatorTransactions::TYPE_EDIT_POLICY:
$object->setEditPolicy($xaction->getNewValue());
break;
case PhabricatorTransactions::TYPE_JOIN_POLICY:
$object->setJoinPolicy($xaction->getNewValue());
break;
case PhabricatorTransactions::TYPE_SPACE:
$object->setSpacePHID($xaction->getNewValue());
break;
case PhabricatorTransactions::TYPE_SUBTYPE:
$object->setEditEngineSubtype($xaction->getNewValue());
break;
}
}
/**
* See @{method::applyBuiltinInternalTransaction}.
*/
protected function applyBuiltinExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_EDGE:
if ($this->getIsInverseEdgeEditor()) {
// If we're writing an inverse edge transaction, don't actually
// do anything. The initiating editor on the other side of the
// transaction will take care of the edge writes.
break;
}
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$src = $object->getPHID();
$const = $xaction->getMetadataValue('edge:type');
foreach ($new as $dst_phid => $edge) {
$new[$dst_phid]['src'] = $src;
}
$editor = new PhabricatorEdgeEditor();
foreach ($old as $dst_phid => $edge) {
if (!empty($new[$dst_phid])) {
if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
continue;
}
}
$editor->removeEdge($src, $const, $dst_phid);
}
foreach ($new as $dst_phid => $edge) {
if (!empty($old[$dst_phid])) {
if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
continue;
}
}
$data = array(
'data' => $edge['data'],
);
$editor->addEdge($src, $const, $dst_phid, $data);
}
$editor->save();
$this->updateWorkboardColumns($object, $const, $old, $new);
break;
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_SPACE:
$this->scrambleFileSecrets($object);
break;
case PhabricatorTransactions::TYPE_HISTORY:
$this->sendHistory = true;
break;
}
}
/**
* Fill in a transaction's common values, like author and content source.
*/
protected function populateTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$actor = $this->getActor();
// TODO: This needs to be more sophisticated once we have meta-policies.
$xaction->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC);
if ($actor->isOmnipotent()) {
$xaction->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
} else {
$xaction->setEditPolicy($this->getActingAsPHID());
}
// If the transaction already has an explicit author PHID, allow it to
// stand. This is used by applications like Owners that hook into the
// post-apply change pipeline.
if (!$xaction->getAuthorPHID()) {
$xaction->setAuthorPHID($this->getActingAsPHID());
}
$xaction->setContentSource($this->getContentSource());
$xaction->attachViewer($actor);
$xaction->attachObject($object);
if ($object->getPHID()) {
$xaction->setObjectPHID($object->getPHID());
}
if ($this->getIsSilent()) {
$xaction->setIsSilentTransaction(true);
}
return $xaction;
}
protected function didApplyInternalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
return $xactions;
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
return $xactions;
}
final protected function didCommitTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
// See T13082. When we're writing edges that imply corresponding inverse
// transactions, apply those inverse transactions now. We have to wait
// until the object we're editing (with this editor) has committed its
// transactions to do this. If we don't, the inverse editor may race,
// build a mail before we actually commit this object, and render "alice
// added an edge: Unknown Object".
if ($type === PhabricatorTransactions::TYPE_EDGE) {
// Don't do anything if we're already an inverse edge editor.
if ($this->getIsInverseEdgeEditor()) {
continue;
}
$edge_const = $xaction->getMetadataValue('edge:type');
$edge_type = PhabricatorEdgeType::getByConstant($edge_const);
if ($edge_type->shouldWriteInverseTransactions()) {
$this->applyInverseEdgeTransactions(
$object,
$xaction,
$edge_type->getInverseEdgeConstant());
}
continue;
}
$xtype = $this->getModularTransactionType($type);
if (!$xtype) {
continue;
}
$xtype = clone $xtype;
$xtype->setStorage($xaction);
$xtype->didCommitTransaction($object, $xaction->getNewValue());
}
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function setContentSourceFromRequest(AphrontRequest $request) {
$this->setRequest($request);
return $this->setContentSource(
PhabricatorContentSource::newFromRequest($request));
}
public function getContentSource() {
return $this->contentSource;
}
public function setRequest(AphrontRequest $request) {
$this->request = $request;
return $this;
}
public function getRequest() {
return $this->request;
}
public function setCancelURI($cancel_uri) {
$this->cancelURI = $cancel_uri;
return $this;
}
public function getCancelURI() {
return $this->cancelURI;
}
final public function applyTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$this->object = $object;
$this->xactions = $xactions;
$this->isNewObject = ($object->getPHID() === null);
$this->validateEditParameters($object, $xactions);
$xactions = $this->newMFATransactions($object, $xactions);
$actor = $this->requireActor();
// NOTE: Some transaction expansion requires that the edited object be
// attached.
foreach ($xactions as $xaction) {
$xaction->attachObject($object);
$xaction->attachViewer($actor);
}
$xactions = $this->expandTransactions($object, $xactions);
$xactions = $this->expandSupportTransactions($object, $xactions);
$xactions = $this->combineTransactions($xactions);
foreach ($xactions as $xaction) {
$xaction = $this->populateTransaction($object, $xaction);
}
$is_preview = $this->getIsPreview();
$read_locking = false;
$transaction_open = false;
if (!$is_preview) {
$errors = array();
$type_map = mgroup($xactions, 'getTransactionType');
foreach ($this->getTransactionTypes() as $type) {
$type_xactions = idx($type_map, $type, array());
$errors[] = $this->validateTransaction($object, $type, $type_xactions);
}
$errors[] = $this->validateAllTransactions($object, $xactions);
$errors[] = $this->validateTransactionsWithExtensions($object, $xactions);
$errors = array_mergev($errors);
$continue_on_missing = $this->getContinueOnMissingFields();
foreach ($errors as $key => $error) {
if ($continue_on_missing && $error->getIsMissingFieldError()) {
unset($errors[$key]);
}
}
if ($errors) {
throw new PhabricatorApplicationTransactionValidationException($errors);
}
if ($this->raiseWarnings) {
$warnings = array();
foreach ($xactions as $xaction) {
if ($this->hasWarnings($object, $xaction)) {
$warnings[] = $xaction;
}
}
if ($warnings) {
throw new PhabricatorApplicationTransactionWarningException(
$warnings);
}
}
}
foreach ($xactions as $xaction) {
$this->adjustTransactionValues($object, $xaction);
}
// Now that we've merged and combined transactions, check for required
// capabilities. Note that we're doing this before filtering
// transactions: if you try to apply an edit which you do not have
// permission to apply, we want to give you a permissions error even
// if the edit would have no effect.
$this->applyCapabilityChecks($object, $xactions);
$xactions = $this->filterTransactions($object, $xactions);
if (!$is_preview) {
$this->hasRequiredMFA = true;
if ($this->getShouldRequireMFA()) {
$this->requireMFA($object, $xactions);
}
if ($object->getID()) {
$this->buildOldRecipientLists($object, $xactions);
$object->openTransaction();
$transaction_open = true;
$object->beginReadLocking();
$read_locking = true;
$object->reload();
}
if ($this->shouldApplyInitialEffects($object, $xactions)) {
if (!$transaction_open) {
$object->openTransaction();
$transaction_open = true;
}
}
}
try {
if ($this->shouldApplyInitialEffects($object, $xactions)) {
$this->applyInitialEffects($object, $xactions);
}
// TODO: Once everything is on EditEngine, just use getIsNewObject() to
// figure this out instead.
$mark_as_create = false;
$create_type = PhabricatorTransactions::TYPE_CREATE;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $create_type) {
$mark_as_create = true;
}
}
if ($mark_as_create) {
foreach ($xactions as $xaction) {
$xaction->setIsCreateTransaction(true);
}
}
$xactions = $this->sortTransactions($xactions);
$file_phids = $this->extractFilePHIDs($object, $xactions);
if ($is_preview) {
$this->loadHandles($xactions);
return $xactions;
}
$comment_editor = id(new PhabricatorApplicationTransactionCommentEditor())
->setActor($actor)
->setActingAsPHID($this->getActingAsPHID())
- ->setContentSource($this->getContentSource());
+ ->setContentSource($this->getContentSource())
+ ->setIsNewComment(true);
if (!$transaction_open) {
$object->openTransaction();
$transaction_open = true;
}
+ // We can technically test any object for CAN_INTERACT, but we can
+ // run into some issues in doing so (for example, in project unit tests).
+ // For now, only test for CAN_INTERACT if the object is explicitly a
+ // lockable object.
+
+ $was_locked = false;
+ if ($object instanceof PhabricatorEditEngineLockableInterface) {
+ $was_locked = !PhabricatorPolicyFilter::canInteract($actor, $object);
+ }
+
foreach ($xactions as $xaction) {
$this->applyInternalEffects($object, $xaction);
}
$xactions = $this->didApplyInternalEffects($object, $xactions);
try {
$object->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
// This callback has an opportunity to throw a better exception,
// so execution may end here.
$this->didCatchDuplicateKeyException($object, $xactions, $ex);
throw $ex;
}
foreach ($xactions as $xaction) {
+ if ($was_locked) {
+ $xaction->setIsLockOverrideTransaction(true);
+ }
+
$xaction->setObjectPHID($object->getPHID());
if ($xaction->getComment()) {
$xaction->setPHID($xaction->generatePHID());
$comment_editor->applyEdit($xaction, $xaction->getComment());
} else {
// TODO: This is a transitional hack to let us migrate edge
// transactions to a more efficient storage format. For now, we're
// going to write a new slim format to the database but keep the old
// bulky format on the objects so we don't have to upgrade all the
// edit logic to the new format yet. See T13051.
$edge_type = PhabricatorTransactions::TYPE_EDGE;
if ($xaction->getTransactionType() == $edge_type) {
$bulky_old = $xaction->getOldValue();
$bulky_new = $xaction->getNewValue();
$record = PhabricatorEdgeChangeRecord::newFromTransaction($xaction);
$slim_old = $record->getModernOldEdgeTransactionData();
$slim_new = $record->getModernNewEdgeTransactionData();
$xaction->setOldValue($slim_old);
$xaction->setNewValue($slim_new);
$xaction->save();
$xaction->setOldValue($bulky_old);
$xaction->setNewValue($bulky_new);
} else {
$xaction->save();
}
}
}
if ($file_phids) {
$this->attachFiles($object, $file_phids);
}
foreach ($xactions as $xaction) {
$this->applyExternalEffects($object, $xaction);
}
$xactions = $this->applyFinalEffects($object, $xactions);
if ($read_locking) {
$object->endReadLocking();
$read_locking = false;
}
if ($transaction_open) {
$object->saveTransaction();
$transaction_open = false;
}
$this->didCommitTransactions($object, $xactions);
} catch (Exception $ex) {
if ($read_locking) {
$object->endReadLocking();
$read_locking = false;
}
if ($transaction_open) {
$object->killTransaction();
$transaction_open = false;
}
throw $ex;
}
// If we need to perform cache engine updates, execute them now.
id(new PhabricatorCacheEngine())
->updateObject($object);
// Now that we've completely applied the core transaction set, try to apply
// Herald rules. Herald rules are allowed to either take direct actions on
// the database (like writing flags), or take indirect actions (like saving
// some targets for CC when we generate mail a little later), or return
// transactions which we'll apply normally using another Editor.
// First, check if *this* is a sub-editor which is itself applying Herald
// rules: if it is, stop working and return so we don't descend into
// madness.
// Otherwise, we're not a Herald editor, so process Herald rules (possibly
// using a Herald editor to apply resulting transactions) and then send out
// mail, notifications, and feed updates about everything.
if ($this->getIsHeraldEditor()) {
// We are the Herald editor, so stop work here and return the updated
// transactions.
return $xactions;
} else if ($this->getIsInverseEdgeEditor()) {
// Do not run Herald if we're just recording that this object was
// mentioned elsewhere. This tends to create Herald side effects which
// feel arbitrary, and can really slow down edits which mention a large
// number of other objects. See T13114.
} else if ($this->shouldApplyHeraldRules($object, $xactions)) {
// We are not the Herald editor, so try to apply Herald rules.
$herald_xactions = $this->applyHeraldRules($object, $xactions);
if ($herald_xactions) {
$xscript_id = $this->getHeraldTranscript()->getID();
foreach ($herald_xactions as $herald_xaction) {
// Don't set a transcript ID if this is a transaction from another
// application or source, like Owners.
if ($herald_xaction->getAuthorPHID()) {
continue;
}
$herald_xaction->setMetadataValue('herald:transcriptID', $xscript_id);
}
// NOTE: We're acting as the omnipotent user because rules deal with
// their own policy issues. We use a synthetic author PHID (the
// Herald application) as the author of record, so that transactions
// will render in a reasonable way ("Herald assigned this task ...").
$herald_actor = PhabricatorUser::getOmnipotentUser();
$herald_phid = id(new PhabricatorHeraldApplication())->getPHID();
// TODO: It would be nice to give transactions a more specific source
// which points at the rule which generated them. You can figure this
// out from transcripts, but it would be cleaner if you didn't have to.
$herald_source = PhabricatorContentSource::newForSource(
PhabricatorHeraldContentSource::SOURCECONST);
- $herald_editor = newv(get_class($this), array())
+ $herald_editor = $this->newEditorCopy()
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
- ->setParentMessageID($this->getParentMessageID())
->setIsHeraldEditor(true)
->setActor($herald_actor)
->setActingAsPHID($herald_phid)
->setContentSource($herald_source);
$herald_xactions = $herald_editor->applyTransactions(
$object,
$herald_xactions);
// Merge the new transactions into the transaction list: we want to
// send email and publish feed stories about them, too.
$xactions = array_merge($xactions, $herald_xactions);
}
// If Herald did not generate transactions, we may still need to handle
// "Send an Email" rules.
$adapter = $this->getHeraldAdapter();
$this->heraldEmailPHIDs = $adapter->getEmailPHIDs();
$this->heraldForcedEmailPHIDs = $adapter->getForcedEmailPHIDs();
$this->webhookMap = $adapter->getWebhookMap();
}
$xactions = $this->didApplyTransactions($object, $xactions);
if ($object instanceof PhabricatorCustomFieldInterface) {
// Maybe this makes more sense to move into the search index itself? For
// now I'm putting it here since I think we might end up with things that
// need it to be up to date once the next page loads, but if we don't go
// there we could move it into search once search moves to the daemons.
// It now happens in the search indexer as well, but the search indexer is
// always daemonized, so the logic above still potentially holds. We could
// possibly get rid of this. The major motivation for putting it in the
// indexer was to enable reindexing to work.
$fields = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
$fields->readFieldsFromStorage($object);
$fields->rebuildIndexes($object);
}
$herald_xscript = $this->getHeraldTranscript();
if ($herald_xscript) {
$herald_header = $herald_xscript->getXHeraldRulesHeader();
$herald_header = HeraldTranscript::saveXHeraldRulesHeader(
$object->getPHID(),
$herald_header);
} else {
$herald_header = HeraldTranscript::loadXHeraldRulesHeader(
$object->getPHID());
}
$this->heraldHeader = $herald_header;
+ // See PHI1134. If we're a subeditor, we don't publish information about
+ // the edit yet. Our parent editor still needs to finish applying
+ // transactions and execute Herald, which may change the information we
+ // publish.
+
+ // For example, Herald actions may change the parent object's title or
+ // visibility, or Herald may apply rules like "Must Encrypt" that affect
+ // email.
+
+ // Once the parent finishes work, it will queue its own publish step and
+ // then queue publish steps for its children.
+
+ $this->publishableObject = $object;
+ $this->publishableTransactions = $xactions;
+ if (!$this->parentEditor) {
+ $this->queuePublishing();
+ }
+
+ return $xactions;
+ }
+
+ final private function queuePublishing() {
+ $object = $this->publishableObject;
+ $xactions = $this->publishableTransactions;
+
+ if (!$object) {
+ throw new Exception(
+ pht(
+ 'Editor method "queuePublishing()" was called, but no publishable '.
+ 'object is present. This Editor is not ready to publish.'));
+ }
+
// We're going to compute some of the data we'll use to publish these
// transactions here, before queueing a worker.
//
// Primarily, this is more correct: we want to publish the object as it
// exists right now. The worker may not execute for some time, and we want
// to use the current To/CC list, not respect any changes which may occur
// between now and when the worker executes.
//
// As a secondary benefit, this tends to reduce the amount of state that
// Editors need to pass into workers.
$object = $this->willPublish($object, $xactions);
if (!$this->getIsSilent()) {
if ($this->shouldSendMail($object, $xactions)) {
$this->mailShouldSend = true;
$this->mailToPHIDs = $this->getMailTo($object);
$this->mailCCPHIDs = $this->getMailCC($object);
$this->mailUnexpandablePHIDs = $this->newMailUnexpandablePHIDs($object);
// Add any recipients who were previously on the notification list
// but were removed by this change.
$this->applyOldRecipientLists();
if ($object instanceof PhabricatorSubscribableInterface) {
$this->mailMutedPHIDs = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorMutedByEdgeType::EDGECONST);
} else {
$this->mailMutedPHIDs = array();
}
$mail_xactions = $this->getTransactionsForMail($object, $xactions);
$stamps = $this->newMailStamps($object, $xactions);
foreach ($stamps as $stamp) {
$this->mailStamps[] = $stamp->toDictionary();
}
}
if ($this->shouldPublishFeedStory($object, $xactions)) {
$this->feedShouldPublish = true;
$this->feedRelatedPHIDs = $this->getFeedRelatedPHIDs(
$object,
$xactions);
$this->feedNotifyPHIDs = $this->getFeedNotifyPHIDs(
$object,
$xactions);
}
}
PhabricatorWorker::scheduleTask(
'PhabricatorApplicationTransactionPublishWorker',
array(
'objectPHID' => $object->getPHID(),
'actorPHID' => $this->getActingAsPHID(),
'xactionPHIDs' => mpull($xactions, 'getPHID'),
'state' => $this->getWorkerState(),
),
array(
'objectPHID' => $object->getPHID(),
'priority' => PhabricatorWorker::PRIORITY_ALERTS,
));
- $this->flushTransactionQueue($object);
+ foreach ($this->subEditors as $sub_editor) {
+ $sub_editor->queuePublishing();
+ }
- return $xactions;
+ $this->flushTransactionQueue($object);
}
protected function didCatchDuplicateKeyException(
PhabricatorLiskDAO $object,
array $xactions,
Exception $ex) {
return;
}
public function publishTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$this->object = $object;
$this->xactions = $xactions;
// Hook for edges or other properties that may need (re-)loading
$object = $this->willPublish($object, $xactions);
// The object might have changed, so reassign it.
$this->object = $object;
$messages = array();
if ($this->mailShouldSend) {
$messages = $this->buildMail($object, $xactions);
}
if ($this->supportsSearch()) {
PhabricatorSearchWorker::queueDocumentForIndexing(
$object->getPHID(),
array(
'transactionPHIDs' => mpull($xactions, 'getPHID'),
));
}
if ($this->feedShouldPublish) {
$mailed = array();
foreach ($messages as $mail) {
foreach ($mail->buildRecipientList() as $phid) {
$mailed[$phid] = $phid;
}
}
$this->publishFeedStory($object, $xactions, $mailed);
}
if ($this->sendHistory) {
$history_mail = $this->buildHistoryMail($object);
if ($history_mail) {
$messages[] = $history_mail;
}
}
// NOTE: This actually sends the mail. We do this last to reduce the chance
// that we send some mail, hit an exception, then send the mail again when
// retrying.
foreach ($messages as $mail) {
$mail->save();
}
$this->queueWebhooks($object, $xactions);
return $xactions;
}
protected function didApplyTransactions($object, array $xactions) {
// Hook for subclasses.
return $xactions;
}
private function loadHandles(array $xactions) {
$phids = array();
foreach ($xactions as $key => $xaction) {
$phids[$key] = $xaction->getRequiredHandlePHIDs();
}
$handles = array();
$merged = array_mergev($phids);
if ($merged) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireActor())
->withPHIDs($merged)
->execute();
}
foreach ($xactions as $key => $xaction) {
$xaction->setHandles(array_select_keys($handles, $phids[$key]));
}
}
private function loadSubscribers(PhabricatorLiskDAO $object) {
if ($object->getPHID() &&
($object instanceof PhabricatorSubscribableInterface)) {
$subs = PhabricatorSubscribersQuery::loadSubscribersForPHID(
$object->getPHID());
$this->subscribers = array_fuse($subs);
} else {
$this->subscribers = array();
}
}
private function validateEditParameters(
PhabricatorLiskDAO $object,
array $xactions) {
if (!$this->getContentSource()) {
throw new PhutilInvalidStateException('setContentSource');
}
// Do a bunch of sanity checks that the incoming transactions are fresh.
// They should be unsaved and have only "transactionType" and "newValue"
// set.
$types = array_fill_keys($this->getTransactionTypes(), true);
assert_instances_of($xactions, 'PhabricatorApplicationTransaction');
foreach ($xactions as $xaction) {
if ($xaction->getPHID() || $xaction->getID()) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht('You can not apply transactions which already have IDs/PHIDs!'));
}
if ($xaction->getObjectPHID()) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'You can not apply transactions which already have %s!',
'objectPHIDs'));
}
if ($xaction->getCommentPHID()) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'You can not apply transactions which already have %s!',
'commentPHIDs'));
}
if ($xaction->getCommentVersion() !== 0) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'You can not apply transactions which already have '.
'commentVersions!'));
}
$expect_value = !$xaction->shouldGenerateOldValue();
$has_value = $xaction->hasOldValue();
// See T13082. In the narrow case of applying inverse edge edits, we
// expect the old value to be populated.
if ($this->getIsInverseEdgeEditor()) {
$expect_value = true;
}
if ($expect_value && !$has_value) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'This transaction is supposed to have an %s set, but it does not!',
'oldValue'));
}
if ($has_value && !$expect_value) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'This transaction should generate its %s automatically, '.
'but has already had one set!',
'oldValue'));
}
$type = $xaction->getTransactionType();
if (empty($types[$type])) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'Transaction has type "%s", but that transaction type is not '.
'supported by this editor (%s).',
$type,
get_class($this)));
}
}
}
private function applyCapabilityChecks(
PhabricatorLiskDAO $object,
array $xactions) {
assert_instances_of($xactions, 'PhabricatorApplicationTransaction');
$can_edit = PhabricatorPolicyCapability::CAN_EDIT;
if ($this->getIsNewObject()) {
// If we're creating a new object, we don't need any special capabilities
// on the object. The actor has already made it through creation checks,
// and objects which haven't been created yet often can not be
// meaningfully tested for capabilities anyway.
$required_capabilities = array();
} else {
if (!$xactions && !$this->xactions) {
// If we aren't doing anything, require CAN_EDIT to improve consistency.
$required_capabilities = array($can_edit);
} else {
$required_capabilities = array();
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if (!$xtype) {
$capabilities = $this->getLegacyRequiredCapabilities($xaction);
} else {
$capabilities = $xtype->getRequiredCapabilities($object, $xaction);
}
// For convenience, we allow flexibility in the return types because
// it's very unusual that a transaction actually requires multiple
// capability checks.
if ($capabilities === null) {
$capabilities = array();
} else {
$capabilities = (array)$capabilities;
}
foreach ($capabilities as $capability) {
$required_capabilities[$capability] = $capability;
}
}
}
}
$required_capabilities = array_fuse($required_capabilities);
$actor = $this->getActor();
if ($required_capabilities) {
id(new PhabricatorPolicyFilter())
->setViewer($actor)
->requireCapabilities($required_capabilities)
->raisePolicyExceptions(true)
->apply(array($object));
}
}
private function getLegacyRequiredCapabilities(
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
// TODO: Comments technically require CAN_INTERACT, but this is
// currently somewhat special and handled through EditEngine. For now,
// don't enforce it here.
return null;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
- // TODO: Removing subscribers other than yourself should probably
- // require CAN_EDIT permission. You can do this via the API but
- // generally can not via the web interface.
+ // Anyone can subscribe to or unsubscribe from anything they can view,
+ // with no other permissions.
+
+ $old = array_fuse($xaction->getOldValue());
+ $new = array_fuse($xaction->getNewValue());
+
+ // To remove users other than yourself, you must be able to edit the
+ // object.
+ $rem = array_diff_key($old, $new);
+ foreach ($rem as $phid) {
+ if ($phid !== $this->getActingAsPHID()) {
+ return PhabricatorPolicyCapability::CAN_EDIT;
+ }
+ }
+
+ // To add users other than yourself, you must be able to interact.
+ // This allows "@mentioning" users to work as long as you can comment
+ // on objects.
+
+ // If you can edit, we return that policy instead so that you can
+ // override a soft lock and still make edits.
+
+ // TODO: This is a little bit hacky. We really want to be able to say
+ // "this requires either interact or edit", but there's currently no
+ // way to specify this kind of requirement.
+
+ $can_edit = PhabricatorPolicyFilter::hasCapability(
+ $this->getActor(),
+ $this->object,
+ PhabricatorPolicyCapability::CAN_EDIT);
+
+ $add = array_diff_key($new, $old);
+ foreach ($add as $phid) {
+ if ($phid !== $this->getActingAsPHID()) {
+ if ($can_edit) {
+ return PhabricatorPolicyCapability::CAN_EDIT;
+ } else {
+ return PhabricatorPolicyCapability::CAN_INTERACT;
+ }
+ }
+ }
+
return null;
case PhabricatorTransactions::TYPE_TOKEN:
// TODO: This technically requires CAN_INTERACT, like comments.
return null;
case PhabricatorTransactions::TYPE_HISTORY:
// This is a special magic transaction which sends you history via
// email and is only partially supported in the upstream. You don't
// need any capabilities to apply it.
return null;
case PhabricatorTransactions::TYPE_MFA:
// Signing a transaction group with MFA does not require permissions
// on its own.
return null;
case PhabricatorTransactions::TYPE_EDGE:
return $this->getLegacyRequiredEdgeCapabilities($xaction);
default:
// For other older (non-modular) transactions, always require exactly
// CAN_EDIT. Transactions which do not need CAN_EDIT or need additional
// capabilities must move to ModularTransactions.
return PhabricatorPolicyCapability::CAN_EDIT;
}
}
private function getLegacyRequiredEdgeCapabilities(
PhabricatorApplicationTransaction $xaction) {
// You don't need to have edit permission on an object to mention it or
// otherwise add a relationship pointing toward it.
if ($this->getIsInverseEdgeEditor()) {
return null;
}
$edge_type = $xaction->getMetadataValue('edge:type');
switch ($edge_type) {
case PhabricatorMutedByEdgeType::EDGECONST:
// At time of writing, you can only write this edge for yourself, so
// you don't need permissions. If you can eventually mute an object
// for other users, this would need to be revisited.
return null;
case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
return null;
case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST:
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$add = array_keys(array_diff_key($new, $old));
$rem = array_keys(array_diff_key($old, $new));
$actor_phid = $this->requireActor()->getPHID();
$is_join = (($add === array($actor_phid)) && !$rem);
$is_leave = (($rem === array($actor_phid)) && !$add);
if ($is_join) {
// You need CAN_JOIN to join a project.
return PhabricatorPolicyCapability::CAN_JOIN;
}
if ($is_leave) {
$object = $this->object;
// You usually don't need any capabilities to leave a project...
if ($object->getIsMembershipLocked()) {
// ...you must be able to edit to leave locked projects, though.
return PhabricatorPolicyCapability::CAN_EDIT;
} else {
return null;
}
}
// You need CAN_EDIT to change members other than yourself.
return PhabricatorPolicyCapability::CAN_EDIT;
case PhabricatorObjectHasWatcherEdgeType::EDGECONST:
// See PHI1024. Watching a project does not require CAN_EDIT.
return null;
default:
return PhabricatorPolicyCapability::CAN_EDIT;
}
}
private function buildSubscribeTransaction(
PhabricatorLiskDAO $object,
array $xactions,
array $changes) {
if (!($object instanceof PhabricatorSubscribableInterface)) {
return null;
}
if ($this->shouldEnableMentions($object, $xactions)) {
// Identify newly mentioned users. We ignore users who were previously
// mentioned so that we don't re-subscribe users after an edit of text
// which mentions them.
$old_texts = mpull($changes, 'getOldValue');
$new_texts = mpull($changes, 'getNewValue');
$old_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(
$this->getActor(),
$old_texts);
$new_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(
$this->getActor(),
$new_texts);
$phids = array_diff($new_phids, $old_phids);
} else {
$phids = array();
}
$this->mentionedPHIDs = $phids;
if ($object->getPHID()) {
// Don't try to subscribe already-subscribed mentions: we want to generate
// a dialog about an action having no effect if the user explicitly adds
// existing CCs, but not if they merely mention existing subscribers.
$phids = array_diff($phids, $this->subscribers);
}
if ($phids) {
$users = id(new PhabricatorPeopleQuery())
->setViewer($this->getActor())
->withPHIDs($phids)
->execute();
$users = mpull($users, null, 'getPHID');
foreach ($phids as $key => $phid) {
- // Do not subscribe mentioned users
- // who do not have VIEW Permissions
- if ($object instanceof PhabricatorPolicyInterface
- && !PhabricatorPolicyFilter::hasCapability(
- $users[$phid],
- $object,
- PhabricatorPolicyCapability::CAN_VIEW)
- ) {
+ $user = idx($users, $phid);
+
+ // Don't subscribe invalid users.
+ if (!$user) {
unset($phids[$key]);
- } else {
- if ($object->isAutomaticallySubscribed($phid)) {
+ continue;
+ }
+
+ // Don't subscribe bots that get mentioned. If users truly intend
+ // to subscribe them, they can add them explicitly, but it's generally
+ // not useful to subscribe bots to objects.
+ if ($user->getIsSystemAgent()) {
+ unset($phids[$key]);
+ continue;
+ }
+
+ // Do not subscribe mentioned users who do not have permission to see
+ // the object.
+ if ($object instanceof PhabricatorPolicyInterface) {
+ $can_view = PhabricatorPolicyFilter::hasCapability(
+ $user,
+ $object,
+ PhabricatorPolicyCapability::CAN_VIEW);
+ if (!$can_view) {
unset($phids[$key]);
+ continue;
}
}
+
+ // Don't subscribe users who are already automatically subscribed.
+ if ($object->isAutomaticallySubscribed($phid)) {
+ unset($phids[$key]);
+ continue;
+ }
}
+
$phids = array_values($phids);
}
- // No else here to properly return null should we unset all subscriber
+
if (!$phids) {
return null;
}
- $xaction = newv(get_class(head($xactions)), array());
- $xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
- $xaction->setNewValue(array('+' => $phids));
+ $xaction = $object->getApplicationTransactionTemplate()
+ ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
+ ->setNewValue(array('+' => $phids));
return $xaction;
}
protected function mergeTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
$type = $u->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$object = $this->object;
return $xtype->mergeTransactions($object, $u, $v);
}
switch ($type) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return $this->mergePHIDOrEdgeTransactions($u, $v);
case PhabricatorTransactions::TYPE_EDGE:
$u_type = $u->getMetadataValue('edge:type');
$v_type = $v->getMetadataValue('edge:type');
if ($u_type == $v_type) {
return $this->mergePHIDOrEdgeTransactions($u, $v);
}
return null;
}
// By default, do not merge the transactions.
return null;
}
/**
* Optionally expand transactions which imply other effects. For example,
* resigning from a revision in Differential implies removing yourself as
* a reviewer.
*/
protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$results = array();
foreach ($xactions as $xaction) {
foreach ($this->expandTransaction($object, $xaction) as $expanded) {
$results[] = $expanded;
}
}
return $results;
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return array($xaction);
}
public function getExpandedSupportTransactions(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$xactions = array($xaction);
$xactions = $this->expandSupportTransactions(
$object,
$xactions);
if (count($xactions) == 1) {
return array();
}
foreach ($xactions as $index => $cxaction) {
if ($cxaction === $xaction) {
unset($xactions[$index]);
break;
}
}
return $xactions;
}
private function expandSupportTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$this->loadSubscribers($object);
$xactions = $this->applyImplicitCC($object, $xactions);
$changes = $this->getRemarkupChanges($xactions);
$subscribe_xaction = $this->buildSubscribeTransaction(
$object,
$xactions,
$changes);
if ($subscribe_xaction) {
$xactions[] = $subscribe_xaction;
}
// TODO: For now, this is just a placeholder.
$engine = PhabricatorMarkupEngine::getEngine('extract');
$engine->setConfig('viewer', $this->requireActor());
$block_xactions = $this->expandRemarkupBlockTransactions(
$object,
$xactions,
$changes,
$engine);
foreach ($block_xactions as $xaction) {
$xactions[] = $xaction;
}
return $xactions;
}
private function getRemarkupChanges(array $xactions) {
$changes = array();
foreach ($xactions as $key => $xaction) {
foreach ($this->getRemarkupChangesFromTransaction($xaction) as $change) {
$changes[] = $change;
}
}
return $changes;
}
private function getRemarkupChangesFromTransaction(
PhabricatorApplicationTransaction $transaction) {
return $transaction->getRemarkupChanges();
}
private function expandRemarkupBlockTransactions(
PhabricatorLiskDAO $object,
array $xactions,
array $changes,
PhutilMarkupEngine $engine) {
$block_xactions = $this->expandCustomRemarkupBlockTransactions(
$object,
$xactions,
$changes,
$engine);
$mentioned_phids = array();
if ($this->shouldEnableMentions($object, $xactions)) {
foreach ($changes as $change) {
// Here, we don't care about processing only new mentions after an edit
// because there is no way for an object to ever "unmention" itself on
// another object, so we can ignore the old value.
$engine->markupText($change->getNewValue());
$mentioned_phids += $engine->getTextMetadata(
PhabricatorObjectRemarkupRule::KEY_MENTIONED_OBJECTS,
array());
}
}
if (!$mentioned_phids) {
return $block_xactions;
}
$mentioned_objects = id(new PhabricatorObjectQuery())
->setViewer($this->getActor())
->withPHIDs($mentioned_phids)
->execute();
$mentionable_phids = array();
if ($this->shouldEnableMentions($object, $xactions)) {
foreach ($mentioned_objects as $mentioned_object) {
if ($mentioned_object instanceof PhabricatorMentionableInterface) {
$mentioned_phid = $mentioned_object->getPHID();
if (idx($this->getUnmentionablePHIDMap(), $mentioned_phid)) {
continue;
}
// don't let objects mention themselves
if ($object->getPHID() && $mentioned_phid == $object->getPHID()) {
continue;
}
$mentionable_phids[$mentioned_phid] = $mentioned_phid;
}
}
}
if ($mentionable_phids) {
$edge_type = PhabricatorObjectMentionsObjectEdgeType::EDGECONST;
$block_xactions[] = newv(get_class(head($xactions)), array())
->setIgnoreOnNoEffect(true)
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_type)
->setNewValue(array('+' => $mentionable_phids));
}
return $block_xactions;
}
protected function expandCustomRemarkupBlockTransactions(
PhabricatorLiskDAO $object,
array $xactions,
array $changes,
PhutilMarkupEngine $engine) {
return array();
}
/**
* Attempt to combine similar transactions into a smaller number of total
* transactions. For example, two transactions which edit the title of an
* object can be merged into a single edit.
*/
private function combineTransactions(array $xactions) {
$stray_comments = array();
$result = array();
$types = array();
foreach ($xactions as $key => $xaction) {
$type = $xaction->getTransactionType();
if (isset($types[$type])) {
foreach ($types[$type] as $other_key) {
$other_xaction = $result[$other_key];
// Don't merge transactions with different authors. For example,
// don't merge Herald transactions and owners transactions.
if ($other_xaction->getAuthorPHID() != $xaction->getAuthorPHID()) {
continue;
}
$merged = $this->mergeTransactions($result[$other_key], $xaction);
if ($merged) {
$result[$other_key] = $merged;
if ($xaction->getComment() &&
($xaction->getComment() !== $merged->getComment())) {
$stray_comments[] = $xaction->getComment();
}
if ($result[$other_key]->getComment() &&
($result[$other_key]->getComment() !== $merged->getComment())) {
$stray_comments[] = $result[$other_key]->getComment();
}
// Move on to the next transaction.
continue 2;
}
}
}
$result[$key] = $xaction;
$types[$type][] = $key;
}
// If we merged any comments away, restore them.
foreach ($stray_comments as $comment) {
$xaction = newv(get_class(head($result)), array());
$xaction->setTransactionType(PhabricatorTransactions::TYPE_COMMENT);
$xaction->setComment($comment);
$result[] = $xaction;
}
return array_values($result);
}
public function mergePHIDOrEdgeTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
$result = $u->getNewValue();
foreach ($v->getNewValue() as $key => $value) {
if ($u->getTransactionType() == PhabricatorTransactions::TYPE_EDGE) {
if (empty($result[$key])) {
$result[$key] = $value;
} else {
// We're merging two lists of edge adds, sets, or removes. Merge
// them by merging individual PHIDs within them.
$merged = $result[$key];
foreach ($value as $dst => $v_spec) {
if (empty($merged[$dst])) {
$merged[$dst] = $v_spec;
} else {
// Two transactions are trying to perform the same operation on
// the same edge. Normalize the edge data and then merge it. This
// allows transactions to specify how data merges execute in a
// precise way.
$u_spec = $merged[$dst];
if (!is_array($u_spec)) {
$u_spec = array('dst' => $u_spec);
}
if (!is_array($v_spec)) {
$v_spec = array('dst' => $v_spec);
}
$ux_data = idx($u_spec, 'data', array());
$vx_data = idx($v_spec, 'data', array());
$merged_data = $this->mergeEdgeData(
$u->getMetadataValue('edge:type'),
$ux_data,
$vx_data);
$u_spec['data'] = $merged_data;
$merged[$dst] = $u_spec;
}
}
$result[$key] = $merged;
}
} else {
$result[$key] = array_merge($value, idx($result, $key, array()));
}
}
$u->setNewValue($result);
// When combining an "ignore" transaction with a normal transaction, make
// sure we don't propagate the "ignore" flag.
if (!$v->getIgnoreOnNoEffect()) {
$u->setIgnoreOnNoEffect(false);
}
return $u;
}
protected function mergeEdgeData($type, array $u, array $v) {
return $v + $u;
}
protected function getPHIDTransactionNewValue(
PhabricatorApplicationTransaction $xaction,
$old = null) {
if ($old !== null) {
$old = array_fuse($old);
} else {
$old = array_fuse($xaction->getOldValue());
}
return $this->getPHIDList($old, $xaction->getNewValue());
}
public function getPHIDList(array $old, array $new) {
$new_add = idx($new, '+', array());
unset($new['+']);
$new_rem = idx($new, '-', array());
unset($new['-']);
$new_set = idx($new, '=', null);
if ($new_set !== null) {
$new_set = array_fuse($new_set);
}
unset($new['=']);
if ($new) {
throw new Exception(
pht(
"Invalid '%s' value for PHID transaction. Value should contain only ".
"keys '%s' (add PHIDs), '%s' (remove PHIDs) and '%s' (set PHIDS).",
'new',
'+',
'-',
'='));
}
$result = array();
foreach ($old as $phid) {
if ($new_set !== null && empty($new_set[$phid])) {
continue;
}
$result[$phid] = $phid;
}
if ($new_set !== null) {
foreach ($new_set as $phid) {
$result[$phid] = $phid;
}
}
foreach ($new_add as $phid) {
$result[$phid] = $phid;
}
foreach ($new_rem as $phid) {
unset($result[$phid]);
}
return array_values($result);
}
protected function getEdgeTransactionNewValue(
PhabricatorApplicationTransaction $xaction) {
$new = $xaction->getNewValue();
$new_add = idx($new, '+', array());
unset($new['+']);
$new_rem = idx($new, '-', array());
unset($new['-']);
$new_set = idx($new, '=', null);
unset($new['=']);
if ($new) {
throw new Exception(
pht(
"Invalid '%s' value for Edge transaction. Value should contain only ".
"keys '%s' (add edges), '%s' (remove edges) and '%s' (set edges).",
'new',
'+',
'-',
'='));
}
$old = $xaction->getOldValue();
$lists = array($new_set, $new_add, $new_rem);
foreach ($lists as $list) {
$this->checkEdgeList($list, $xaction->getMetadataValue('edge:type'));
}
$result = array();
foreach ($old as $dst_phid => $edge) {
if ($new_set !== null && empty($new_set[$dst_phid])) {
continue;
}
$result[$dst_phid] = $this->normalizeEdgeTransactionValue(
$xaction,
$edge,
$dst_phid);
}
if ($new_set !== null) {
foreach ($new_set as $dst_phid => $edge) {
$result[$dst_phid] = $this->normalizeEdgeTransactionValue(
$xaction,
$edge,
$dst_phid);
}
}
foreach ($new_add as $dst_phid => $edge) {
$result[$dst_phid] = $this->normalizeEdgeTransactionValue(
$xaction,
$edge,
$dst_phid);
}
foreach ($new_rem as $dst_phid => $edge) {
unset($result[$dst_phid]);
}
return $result;
}
private function checkEdgeList($list, $edge_type) {
if (!$list) {
return;
}
foreach ($list as $key => $item) {
if (phid_get_type($key) === PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
throw new Exception(
pht(
'Edge transactions must have destination PHIDs as in edge '.
'lists (found key "%s" on transaction of type "%s").',
$key,
$edge_type));
}
if (!is_array($item) && $item !== $key) {
throw new Exception(
pht(
'Edge transactions must have PHIDs or edge specs as values '.
'(found value "%s" on transaction of type "%s").',
$item,
$edge_type));
}
}
}
private function normalizeEdgeTransactionValue(
PhabricatorApplicationTransaction $xaction,
$edge,
$dst_phid) {
if (!is_array($edge)) {
if ($edge != $dst_phid) {
throw new Exception(
pht(
'Transaction edge data must either be the edge PHID or an edge '.
'specification dictionary.'));
}
$edge = array();
} else {
foreach ($edge as $key => $value) {
switch ($key) {
case 'src':
case 'dst':
case 'type':
case 'data':
case 'dateCreated':
case 'dateModified':
case 'seq':
case 'dataID':
break;
default:
throw new Exception(
pht(
'Transaction edge specification contains unexpected key "%s".',
$key));
}
}
}
$edge['dst'] = $dst_phid;
$edge_type = $xaction->getMetadataValue('edge:type');
if (empty($edge['type'])) {
$edge['type'] = $edge_type;
} else {
if ($edge['type'] != $edge_type) {
$this_type = $edge['type'];
throw new Exception(
pht(
"Edge transaction includes edge of type '%s', but ".
"transaction is of type '%s'. Each edge transaction ".
"must alter edges of only one type.",
$this_type,
$edge_type));
}
}
if (!isset($edge['data'])) {
$edge['data'] = array();
}
return $edge;
}
protected function sortTransactions(array $xactions) {
$head = array();
$tail = array();
// Move bare comments to the end, so the actions precede them.
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
if ($type == PhabricatorTransactions::TYPE_COMMENT) {
$tail[] = $xaction;
} else {
$head[] = $xaction;
}
}
return array_values(array_merge($head, $tail));
}
protected function filterTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$type_comment = PhabricatorTransactions::TYPE_COMMENT;
$type_mfa = PhabricatorTransactions::TYPE_MFA;
$no_effect = array();
$has_comment = false;
$any_effect = false;
$meta_xactions = array();
foreach ($xactions as $key => $xaction) {
if ($xaction->getTransactionType() === $type_mfa) {
$meta_xactions[$key] = $xaction;
continue;
}
if ($this->transactionHasEffect($object, $xaction)) {
if ($xaction->getTransactionType() != $type_comment) {
$any_effect = true;
}
} else if ($xaction->getIgnoreOnNoEffect()) {
unset($xactions[$key]);
} else {
$no_effect[$key] = $xaction;
}
if ($xaction->hasComment()) {
$has_comment = true;
}
}
// If every transaction is a meta-transaction applying to the transaction
// group, these transactions are junk.
if (count($meta_xactions) == count($xactions)) {
$no_effect = $xactions;
$any_effect = false;
}
if (!$no_effect) {
return $xactions;
}
// If none of the transactions have an effect, the meta-transactions also
// have no effect. Add them to the "no effect" list so we get a full set
// of errors for everything.
if (!$any_effect) {
$no_effect += $meta_xactions;
}
if (!$this->getContinueOnNoEffect() && !$this->getIsPreview()) {
throw new PhabricatorApplicationTransactionNoEffectException(
$no_effect,
$any_effect,
$has_comment);
}
if (!$any_effect && !$has_comment) {
// If we only have empty comment transactions, just drop them all.
return array();
}
foreach ($no_effect as $key => $xaction) {
if ($xaction->hasComment()) {
$xaction->setTransactionType($type_comment);
$xaction->setOldValue(null);
$xaction->setNewValue(null);
} else {
unset($xactions[$key]);
}
}
return $xactions;
}
/**
* Hook for validating transactions. This callback will be invoked for each
* available transaction type, even if an edit does not apply any transactions
* of that type. This allows you to raise exceptions when required fields are
* missing, by detecting that the object has no field value and there is no
* transaction which sets one.
*
* @param PhabricatorLiskDAO Object being edited.
* @param string Transaction type to validate.
* @param list<PhabricatorApplicationTransaction> Transactions of given type,
* which may be empty if the edit does not apply any transactions of the
* given type.
* @return list<PhabricatorApplicationTransactionValidationError> List of
* validation errors.
*/
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = array();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$errors[] = $xtype->validateTransactions($object, $xactions);
}
switch ($type) {
case PhabricatorTransactions::TYPE_VIEW_POLICY:
$errors[] = $this->validatePolicyTransaction(
$object,
$xactions,
$type,
PhabricatorPolicyCapability::CAN_VIEW);
break;
case PhabricatorTransactions::TYPE_EDIT_POLICY:
$errors[] = $this->validatePolicyTransaction(
$object,
$xactions,
$type,
PhabricatorPolicyCapability::CAN_EDIT);
break;
case PhabricatorTransactions::TYPE_SPACE:
$errors[] = $this->validateSpaceTransactions(
$object,
$xactions,
$type);
break;
case PhabricatorTransactions::TYPE_SUBTYPE:
$errors[] = $this->validateSubtypeTransactions(
$object,
$xactions,
$type);
break;
case PhabricatorTransactions::TYPE_MFA:
$errors[] = $this->validateMFATransactions(
$object,
$xactions,
$type);
break;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$groups = array();
foreach ($xactions as $xaction) {
$groups[$xaction->getMetadataValue('customfield:key')][] = $xaction;
}
$field_list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_EDIT);
$field_list->setViewer($this->getActor());
$role_xactions = PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS;
foreach ($field_list->getFields() as $field) {
if (!$field->shouldEnableForRole($role_xactions)) {
continue;
}
$errors[] = $field->validateApplicationTransactions(
$this,
$type,
idx($groups, $field->getFieldKey(), array()));
}
break;
}
return array_mergev($errors);
}
public function validatePolicyTransaction(
PhabricatorLiskDAO $object,
array $xactions,
$transaction_type,
$capability) {
$actor = $this->requireActor();
$errors = array();
// Note $this->xactions is necessary; $xactions is $this->xactions of
// $transaction_type
$policy_object = $this->adjustObjectForPolicyChecks(
$object,
$this->xactions);
// Make sure the user isn't editing away their ability to $capability this
// object.
foreach ($xactions as $xaction) {
try {
PhabricatorPolicyFilter::requireCapabilityWithForcedPolicy(
$actor,
$policy_object,
$capability,
$xaction->getNewValue());
} catch (PhabricatorPolicyException $ex) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$transaction_type,
pht('Invalid'),
pht(
'You can not select this %s policy, because you would no longer '.
'be able to %s the object.',
$capability,
$capability),
$xaction);
}
}
if ($this->getIsNewObject()) {
if (!$xactions) {
$has_capability = PhabricatorPolicyFilter::hasCapability(
$actor,
$policy_object,
$capability);
if (!$has_capability) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$transaction_type,
pht('Invalid'),
pht(
'The selected %s policy excludes you. Choose a %s policy '.
'which allows you to %s the object.',
$capability,
$capability,
$capability));
}
}
}
return $errors;
}
private function validateSpaceTransactions(
PhabricatorLiskDAO $object,
array $xactions,
$transaction_type) {
$errors = array();
$actor = $this->getActor();
$has_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($actor);
$actor_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces($actor);
$active_spaces = PhabricatorSpacesNamespaceQuery::getViewerActiveSpaces(
$actor);
foreach ($xactions as $xaction) {
$space_phid = $xaction->getNewValue();
if ($space_phid === null) {
if (!$has_spaces) {
// The install doesn't have any spaces, so this is fine.
continue;
}
// The install has some spaces, so every object needs to be put
// in a valid space.
$errors[] = new PhabricatorApplicationTransactionValidationError(
$transaction_type,
pht('Invalid'),
pht('You must choose a space for this object.'),
$xaction);
continue;
}
// If the PHID isn't `null`, it needs to be a valid space that the
// viewer can see.
if (empty($actor_spaces[$space_phid])) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$transaction_type,
pht('Invalid'),
pht(
'You can not shift this object in the selected space, because '.
'the space does not exist or you do not have access to it.'),
$xaction);
} else if (empty($active_spaces[$space_phid])) {
// It's OK to edit objects in an archived space, so just move on if
// we aren't adjusting the value.
$old_space_phid = $this->getTransactionOldValue($object, $xaction);
if ($space_phid == $old_space_phid) {
continue;
}
$errors[] = new PhabricatorApplicationTransactionValidationError(
$transaction_type,
pht('Archived'),
pht(
'You can not shift this object into the selected space, because '.
'the space is archived. Objects can not be created inside (or '.
'moved into) archived spaces.'),
$xaction);
}
}
return $errors;
}
private function validateSubtypeTransactions(
PhabricatorLiskDAO $object,
array $xactions,
$transaction_type) {
$errors = array();
$map = $object->newEditEngineSubtypeMap();
$old = $object->getEditEngineSubtype();
foreach ($xactions as $xaction) {
$new = $xaction->getNewValue();
if ($old == $new) {
continue;
}
if (!$map->isValidSubtype($new)) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$transaction_type,
pht('Invalid'),
pht(
'The subtype "%s" is not a valid subtype.',
$new),
$xaction);
continue;
}
}
return $errors;
}
private function validateMFATransactions(
PhabricatorLiskDAO $object,
array $xactions,
$transaction_type) {
$errors = array();
$factors = id(new PhabricatorAuthFactorConfigQuery())
->setViewer($this->getActor())
->withUserPHIDs(array($this->getActingAsPHID()))
->withFactorProviderStatuses(
array(
PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
PhabricatorAuthFactorProviderStatus::STATUS_DEPRECATED,
))
->execute();
foreach ($xactions as $xaction) {
if (!$factors) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$transaction_type,
pht('No MFA'),
pht(
'You do not have any MFA factors attached to your account, so '.
'you can not sign this transaction group with MFA. Add MFA to '.
'your account in Settings.'),
$xaction);
}
}
if ($xactions) {
$this->setShouldRequireMFA(true);
}
return $errors;
}
protected function adjustObjectForPolicyChecks(
PhabricatorLiskDAO $object,
array $xactions) {
$copy = clone $object;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$clone_xaction = clone $xaction;
$clone_xaction->setOldValue(array_values($this->subscribers));
$clone_xaction->setNewValue(
$this->getPHIDTransactionNewValue(
$clone_xaction));
PhabricatorPolicyRule::passTransactionHintToRule(
$copy,
new PhabricatorSubscriptionsSubscribersPolicyRule(),
array_fuse($clone_xaction->getNewValue()));
break;
case PhabricatorTransactions::TYPE_SPACE:
$space_phid = $this->getTransactionNewValue($object, $xaction);
$copy->setSpacePHID($space_phid);
break;
}
}
return $copy;
}
protected function validateAllTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
return array();
}
/**
* Check for a missing text field.
*
* A text field is missing if the object has no value and there are no
* transactions which set a value, or if the transactions remove the value.
* This method is intended to make implementing @{method:validateTransaction}
* more convenient:
*
* $missing = $this->validateIsEmptyTextField(
* $object->getName(),
* $xactions);
*
* This will return `true` if the net effect of the object and transactions
* is an empty field.
*
* @param wild Current field value.
* @param list<PhabricatorApplicationTransaction> Transactions editing the
* field.
* @return bool True if the field will be an empty text field after edits.
*/
protected function validateIsEmptyTextField($field_value, array $xactions) {
if (strlen($field_value) && empty($xactions)) {
return false;
}
if ($xactions && strlen(last($xactions)->getNewValue())) {
return false;
}
return true;
}
/* -( Implicit CCs )------------------------------------------------------- */
/**
* When a user interacts with an object, we might want to add them to CC.
*/
final public function applyImplicitCC(
PhabricatorLiskDAO $object,
array $xactions) {
if (!($object instanceof PhabricatorSubscribableInterface)) {
// If the object isn't subscribable, we can't CC them.
return $xactions;
}
$actor_phid = $this->getActingAsPHID();
$type_user = PhabricatorPeopleUserPHIDType::TYPECONST;
if (phid_get_type($actor_phid) != $type_user) {
// Transactions by application actors like Herald, Harbormaster and
// Diffusion should not CC the applications.
return $xactions;
}
if ($object->isAutomaticallySubscribed($actor_phid)) {
// If they're auto-subscribed, don't CC them.
return $xactions;
}
$should_cc = false;
foreach ($xactions as $xaction) {
if ($this->shouldImplyCC($object, $xaction)) {
$should_cc = true;
break;
}
}
if (!$should_cc) {
// Only some types of actions imply a CC (like adding a comment).
return $xactions;
}
if ($object->getPHID()) {
if (isset($this->subscribers[$actor_phid])) {
// If the user is already subscribed, don't implicitly CC them.
return $xactions;
}
$unsub = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorObjectHasUnsubscriberEdgeType::EDGECONST);
$unsub = array_fuse($unsub);
if (isset($unsub[$actor_phid])) {
// If the user has previously unsubscribed from this object explicitly,
// don't implicitly CC them.
return $xactions;
}
}
+ $actor = $this->getActor();
+
+ $user = id(new PhabricatorPeopleQuery())
+ ->setViewer($actor)
+ ->withPHIDs(array($actor_phid))
+ ->executeOne();
+ if (!$user) {
+ return $xactions;
+ }
+
+ // When a bot acts (usually via the API), don't automatically subscribe
+ // them as a side effect. They can always subscribe explicitly if they
+ // want, and bot subscriptions normally just clutter things up since bots
+ // usually do not read email.
+ if ($user->getIsSystemAgent()) {
+ return $xactions;
+ }
+
$xaction = newv(get_class(head($xactions)), array());
$xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
$xaction->setNewValue(array('+' => array($actor_phid)));
array_unshift($xactions, $xaction);
return $xactions;
}
protected function shouldImplyCC(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return $xaction->isCommentTransaction();
}
/* -( Sending Mail )------------------------------------------------------- */
/**
* @task mail
*/
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
/**
* @task mail
*/
private function buildMail(
PhabricatorLiskDAO $object,
array $xactions) {
$email_to = $this->mailToPHIDs;
$email_cc = $this->mailCCPHIDs;
$email_cc = array_merge($email_cc, $this->heraldEmailPHIDs);
$unexpandable = $this->mailUnexpandablePHIDs;
if (!is_array($unexpandable)) {
$unexpandable = array();
}
$messages = $this->buildMailWithRecipients(
$object,
$xactions,
$email_to,
$email_cc,
$unexpandable);
$this->runHeraldMailRules($messages);
return $messages;
}
private function buildMailWithRecipients(
PhabricatorLiskDAO $object,
array $xactions,
array $email_to,
array $email_cc,
array $unexpandable) {
$targets = $this->buildReplyHandler($object)
->setUnexpandablePHIDs($unexpandable)
->getMailTargets($email_to, $email_cc);
// Set this explicitly before we start swapping out the effective actor.
$this->setActingAsPHID($this->getActingAsPHID());
$messages = array();
foreach ($targets as $target) {
$original_actor = $this->getActor();
$viewer = $target->getViewer();
$this->setActor($viewer);
$locale = PhabricatorEnv::beginScopedLocale($viewer->getTranslation());
$caught = null;
$mail = null;
try {
// Reload handles for the new viewer.
$this->loadHandles($xactions);
$mail = $this->buildMailForTarget($object, $xactions, $target);
if ($mail) {
if ($this->mustEncrypt) {
$mail
->setMustEncrypt(true)
->setMustEncryptReasons($this->mustEncrypt);
}
}
} catch (Exception $ex) {
$caught = $ex;
}
$this->setActor($original_actor);
unset($locale);
if ($caught) {
throw $ex;
}
if ($mail) {
$messages[] = $mail;
}
}
return $messages;
}
protected function getTransactionsForMail(
PhabricatorLiskDAO $object,
array $xactions) {
return $xactions;
}
private function buildMailForTarget(
PhabricatorLiskDAO $object,
array $xactions,
PhabricatorMailTarget $target) {
// Check if any of the transactions are visible for this viewer. If we
// don't have any visible transactions, don't send the mail.
$any_visible = false;
foreach ($xactions as $xaction) {
if (!$xaction->shouldHideForMail($xactions)) {
$any_visible = true;
break;
}
}
if (!$any_visible) {
return null;
}
$mail_xactions = $this->getTransactionsForMail($object, $xactions);
$mail = $this->buildMailTemplate($object);
$body = $this->buildMailBody($object, $mail_xactions);
$mail_tags = $this->getMailTags($object, $mail_xactions);
$action = $this->getMailAction($object, $mail_xactions);
$stamps = $this->generateMailStamps($object, $this->mailStamps);
if (PhabricatorEnv::getEnvConfig('metamta.email-preferences')) {
$this->addEmailPreferenceSectionToMailBody(
$body,
$object,
$mail_xactions);
}
$muted_phids = $this->mailMutedPHIDs;
if (!is_array($muted_phids)) {
$muted_phids = array();
}
$mail
->setSensitiveContent(false)
->setFrom($this->getActingAsPHID())
->setSubjectPrefix($this->getMailSubjectPrefix())
->setVarySubjectPrefix('['.$action.']')
->setThreadID($this->getMailThreadID($object), $this->getIsNewObject())
->setRelatedPHID($object->getPHID())
->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
->setMutedPHIDs($muted_phids)
->setForceHeraldMailRecipientPHIDs($this->heraldForcedEmailPHIDs)
->setMailTags($mail_tags)
->setIsBulk(true)
->setBody($body->render())
->setHTMLBody($body->renderHTML());
foreach ($body->getAttachments() as $attachment) {
$mail->addAttachment($attachment);
}
if ($this->heraldHeader) {
$mail->addHeader('X-Herald-Rules', $this->heraldHeader);
}
if ($object instanceof PhabricatorProjectInterface) {
$this->addMailProjectMetadata($object, $mail);
}
if ($this->getParentMessageID()) {
$mail->setParentMessageID($this->getParentMessageID());
}
// If we have stamps, attach the raw dictionary version (not the actual
// objects) to the mail so that debugging tools can see what we used to
// render the final list.
if ($this->mailStamps) {
$mail->setMailStampMetadata($this->mailStamps);
}
// If we have rendered stamps, attach them to the mail.
if ($stamps) {
$mail->setMailStamps($stamps);
}
return $target->willSendMail($mail);
}
private function addMailProjectMetadata(
PhabricatorLiskDAO $object,
PhabricatorMetaMTAMail $template) {
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
if (!$project_phids) {
return;
}
// TODO: This viewer isn't quite right. It would be slightly better to use
// the mail recipient, but that's not very easy given the way rendering
// works today.
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireActor())
->withPHIDs($project_phids)
->execute();
$project_tags = array();
foreach ($handles as $handle) {
if (!$handle->isComplete()) {
continue;
}
$project_tags[] = '<'.$handle->getObjectName().'>';
}
if (!$project_tags) {
return;
}
$project_tags = implode(', ', $project_tags);
$template->addHeader('X-Phabricator-Projects', $project_tags);
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
return $object->getPHID();
}
/**
* @task mail
*/
protected function getStrongestAction(
PhabricatorLiskDAO $object,
array $xactions) {
return last(msort($xactions, 'getActionStrength'));
}
/**
* @task mail
*/
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
throw new Exception(pht('Capability not supported.'));
}
/**
* @task mail
*/
protected function getMailSubjectPrefix() {
throw new Exception(pht('Capability not supported.'));
}
/**
* @task mail
*/
protected function getMailTags(
PhabricatorLiskDAO $object,
array $xactions) {
$tags = array();
foreach ($xactions as $xaction) {
$tags[] = $xaction->getMailTags();
}
return array_mergev($tags);
}
/**
* @task mail
*/
public function getMailTagsMap() {
// TODO: We should move shared mail tags, like "comment", here.
return array();
}
/**
* @task mail
*/
protected function getMailAction(
PhabricatorLiskDAO $object,
array $xactions) {
return $this->getStrongestAction($object, $xactions)->getActionName();
}
/**
* @task mail
*/
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
throw new Exception(pht('Capability not supported.'));
}
/**
* @task mail
*/
protected function getMailTo(PhabricatorLiskDAO $object) {
throw new Exception(pht('Capability not supported.'));
}
protected function newMailUnexpandablePHIDs(PhabricatorLiskDAO $object) {
return array();
}
/**
* @task mail
*/
protected function getMailCC(PhabricatorLiskDAO $object) {
$phids = array();
$has_support = false;
if ($object instanceof PhabricatorSubscribableInterface) {
$phid = $object->getPHID();
$phids[] = PhabricatorSubscribersQuery::loadSubscribersForPHID($phid);
$has_support = true;
}
if ($object instanceof PhabricatorProjectInterface) {
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
if ($project_phids) {
$projects = id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($project_phids)
->needWatchers(true)
->execute();
$watcher_phids = array();
foreach ($projects as $project) {
foreach ($project->getAllAncestorWatcherPHIDs() as $phid) {
$watcher_phids[$phid] = $phid;
}
}
if ($watcher_phids) {
// We need to do a visibility check for all the watchers, as
// watching a project is not a guarantee that you can see objects
// associated with it.
$users = id(new PhabricatorPeopleQuery())
->setViewer($this->requireActor())
->withPHIDs($watcher_phids)
->execute();
$watchers = array();
foreach ($users as $user) {
$can_see = PhabricatorPolicyFilter::hasCapability(
$user,
$object,
PhabricatorPolicyCapability::CAN_VIEW);
if ($can_see) {
$watchers[] = $user->getPHID();
}
}
$phids[] = $watchers;
}
}
$has_support = true;
}
if (!$has_support) {
throw new Exception(
pht('The object being edited does not implement any standard '.
'interfaces (like PhabricatorSubscribableInterface) which allow '.
'CCs to be generated automatically. Override the "getMailCC()" '.
'method and generate CCs explicitly.'));
}
return array_mergev($phids);
}
/**
* @task mail
*/
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = id(new PhabricatorMetaMTAMailBody())
->setViewer($this->requireActor())
->setContextObject($object);
$this->addHeadersAndCommentsToMailBody($body, $xactions);
$this->addCustomFieldsToMailBody($body, $object, $xactions);
return $body;
}
/**
* @task mail
*/
protected function addEmailPreferenceSectionToMailBody(
PhabricatorMetaMTAMailBody $body,
PhabricatorLiskDAO $object,
array $xactions) {
$href = PhabricatorEnv::getProductionURI(
'/settings/panel/emailpreferences/');
$body->addLinkSection(pht('EMAIL PREFERENCES'), $href);
}
/**
* @task mail
*/
protected function addHeadersAndCommentsToMailBody(
PhabricatorMetaMTAMailBody $body,
array $xactions,
$object_label = null,
$object_href = null) {
// First, remove transactions which shouldn't be rendered in mail.
foreach ($xactions as $key => $xaction) {
if ($xaction->shouldHideForMail($xactions)) {
unset($xactions[$key]);
}
}
$headers = array();
$headers_html = array();
$comments = array();
$details = array();
$seen_comment = false;
foreach ($xactions as $xaction) {
// Most mail has zero or one comments. In these cases, we render the
// "alice added a comment." transaction in the header, like a normal
// transaction.
// Some mail, like Differential undraft mail or "!history" mail, may
// have two or more comments. In these cases, we'll put the first
// "alice added a comment." transaction in the header normally, but
// move the other transactions down so they provide context above the
// actual comment.
$comment = $this->getBodyForTextMail($xaction);
if ($comment !== null) {
$is_comment = true;
$comments[] = array(
'xaction' => $xaction,
'comment' => $comment,
'initial' => !$seen_comment,
);
} else {
$is_comment = false;
}
if (!$is_comment || !$seen_comment) {
$header = $this->getTitleForTextMail($xaction);
if ($header !== null) {
$headers[] = $header;
}
$header_html = $this->getTitleForHTMLMail($xaction);
if ($header_html !== null) {
$headers_html[] = $header_html;
}
}
if ($xaction->hasChangeDetailsForMail()) {
$details[] = $xaction;
}
if ($is_comment) {
$seen_comment = true;
}
}
$headers_text = implode("\n", $headers);
$body->addRawPlaintextSection($headers_text);
$headers_html = phutil_implode_html(phutil_tag('br'), $headers_html);
$header_button = null;
if ($object_label !== null) {
$button_style = array(
'text-decoration: none;',
'padding: 4px 8px;',
'margin: 0 8px 8px;',
'float: right;',
'color: #464C5C;',
'font-weight: bold;',
'border-radius: 3px;',
'background-color: #F7F7F9;',
'background-image: linear-gradient(to bottom,#fff,#f1f0f1);',
'display: inline-block;',
'border: 1px solid rgba(71,87,120,.2);',
);
$header_button = phutil_tag(
'a',
array(
'style' => implode(' ', $button_style),
'href' => $object_href,
),
$object_label);
}
$xactions_style = array();
$header_action = phutil_tag(
'td',
array(),
$header_button);
$header_action = phutil_tag(
'td',
array(
'style' => implode(' ', $xactions_style),
),
array(
$headers_html,
// Add an extra newline to prevent the "View Object" button from
// running into the transaction text in Mail.app text snippet
// previews.
"\n",
));
$headers_html = phutil_tag(
'table',
array(),
phutil_tag('tr', array(), array($header_action, $header_button)));
$body->addRawHTMLSection($headers_html);
foreach ($comments as $spec) {
$xaction = $spec['xaction'];
$comment = $spec['comment'];
$is_initial = $spec['initial'];
// If this is not the first comment in the mail, add the header showing
// who wrote the comment immediately above the comment.
if (!$is_initial) {
$header = $this->getTitleForTextMail($xaction);
if ($header !== null) {
$body->addRawPlaintextSection($header);
}
$header_html = $this->getTitleForHTMLMail($xaction);
if ($header_html !== null) {
$body->addRawHTMLSection($header_html);
}
}
$body->addRemarkupSection(null, $comment);
}
foreach ($details as $xaction) {
$details = $xaction->renderChangeDetailsForMail($body->getViewer());
if ($details !== null) {
$label = $this->getMailDiffSectionHeader($xaction);
$body->addHTMLSection($label, $details);
}
}
}
private function getMailDiffSectionHeader($xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
return $xtype->getMailDiffSectionHeader();
}
return pht('EDIT DETAILS');
}
/**
* @task mail
*/
protected function addCustomFieldsToMailBody(
PhabricatorMetaMTAMailBody $body,
PhabricatorLiskDAO $object,
array $xactions) {
if ($object instanceof PhabricatorCustomFieldInterface) {
$field_list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_TRANSACTIONMAIL);
$field_list->setViewer($this->getActor());
$field_list->readFieldsFromStorage($object);
foreach ($field_list->getFields() as $field) {
$field->updateTransactionMailBody(
$body,
$this,
$xactions);
}
}
}
/**
* @task mail
*/
private function runHeraldMailRules(array $messages) {
foreach ($messages as $message) {
$engine = new HeraldEngine();
$adapter = id(new PhabricatorMailOutboundMailHeraldAdapter())
->setObject($message);
$rules = $engine->loadRulesForAdapter($adapter);
$effects = $engine->applyRules($rules, $adapter);
$engine->applyEffects($effects, $adapter, $rules);
}
}
/* -( Publishing Feed Stories )-------------------------------------------- */
/**
* @task feed
*/
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
/**
* @task feed
*/
protected function getFeedStoryType() {
return 'PhabricatorApplicationTransactionFeedStory';
}
/**
* @task feed
*/
protected function getFeedRelatedPHIDs(
PhabricatorLiskDAO $object,
array $xactions) {
$phids = array(
$object->getPHID(),
$this->getActingAsPHID(),
);
if ($object instanceof PhabricatorProjectInterface) {
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
foreach ($project_phids as $project_phid) {
$phids[] = $project_phid;
}
}
return $phids;
}
/**
* @task feed
*/
protected function getFeedNotifyPHIDs(
PhabricatorLiskDAO $object,
array $xactions) {
// If some transactions are forcing notification delivery, add the forced
// recipients to the notify list.
$force_list = array();
foreach ($xactions as $xaction) {
$force_phids = $xaction->getForceNotifyPHIDs();
if (!$force_phids) {
continue;
}
foreach ($force_phids as $force_phid) {
$force_list[] = $force_phid;
}
}
$to_list = $this->getMailTo($object);
$cc_list = $this->getMailCC($object);
$full_list = array_merge($force_list, $to_list, $cc_list);
$full_list = array_fuse($full_list);
return array_keys($full_list);
}
/**
* @task feed
*/
protected function getFeedStoryData(
PhabricatorLiskDAO $object,
array $xactions) {
$xactions = msort($xactions, 'getActionStrength');
$xactions = array_reverse($xactions);
return array(
'objectPHID' => $object->getPHID(),
'transactionPHIDs' => mpull($xactions, 'getPHID'),
);
}
/**
* @task feed
*/
protected function publishFeedStory(
PhabricatorLiskDAO $object,
array $xactions,
array $mailed_phids) {
// Remove transactions which don't publish feed stories or notifications.
// These never show up anywhere, so we don't need to do anything with them.
foreach ($xactions as $key => $xaction) {
if (!$xaction->shouldHideForFeed()) {
continue;
}
if (!$xaction->shouldHideForNotifications()) {
continue;
}
unset($xactions[$key]);
}
if (!$xactions) {
return;
}
$related_phids = $this->feedRelatedPHIDs;
$subscribed_phids = $this->feedNotifyPHIDs;
// Remove muted users from the subscription list so they don't get
// notifications, either.
$muted_phids = $this->mailMutedPHIDs;
if (!is_array($muted_phids)) {
$muted_phids = array();
}
$subscribed_phids = array_fuse($subscribed_phids);
foreach ($muted_phids as $muted_phid) {
unset($subscribed_phids[$muted_phid]);
}
$subscribed_phids = array_values($subscribed_phids);
$story_type = $this->getFeedStoryType();
$story_data = $this->getFeedStoryData($object, $xactions);
$unexpandable_phids = $this->mailUnexpandablePHIDs;
if (!is_array($unexpandable_phids)) {
$unexpandable_phids = array();
}
id(new PhabricatorFeedStoryPublisher())
->setStoryType($story_type)
->setStoryData($story_data)
->setStoryTime(time())
->setStoryAuthorPHID($this->getActingAsPHID())
->setRelatedPHIDs($related_phids)
->setPrimaryObjectPHID($object->getPHID())
->setSubscribedPHIDs($subscribed_phids)
->setUnexpandablePHIDs($unexpandable_phids)
->setMailRecipientPHIDs($mailed_phids)
->setMailTags($this->getMailTags($object, $xactions))
->publish();
}
/* -( Search Index )------------------------------------------------------- */
/**
* @task search
*/
protected function supportsSearch() {
return false;
}
/* -( Herald Integration )-------------------------------------------------- */
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
throw new Exception(pht('No herald adapter specified.'));
}
private function setHeraldAdapter(HeraldAdapter $adapter) {
$this->heraldAdapter = $adapter;
return $this;
}
protected function getHeraldAdapter() {
return $this->heraldAdapter;
}
private function setHeraldTranscript(HeraldTranscript $transcript) {
$this->heraldTranscript = $transcript;
return $this;
}
protected function getHeraldTranscript() {
return $this->heraldTranscript;
}
private function applyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
$adapter = $this->buildHeraldAdapter($object, $xactions)
->setContentSource($this->getContentSource())
->setIsNewObject($this->getIsNewObject())
->setActingAsPHID($this->getActingAsPHID())
->setAppliedTransactions($xactions);
if ($this->getApplicationEmail()) {
$adapter->setApplicationEmail($this->getApplicationEmail());
}
// If this editor is operating in silent mode, tell Herald that we aren't
// going to send any mail. This allows it to skip "the first time this
// rule matches, send me an email" rules which would otherwise match even
// though we aren't going to send any mail.
if ($this->getIsSilent()) {
$adapter->setForbiddenAction(
HeraldMailableState::STATECONST,
HeraldCoreStateReasons::REASON_SILENT);
}
$xscript = HeraldEngine::loadAndApplyRules($adapter);
$this->setHeraldAdapter($adapter);
$this->setHeraldTranscript($xscript);
if ($adapter instanceof HarbormasterBuildableAdapterInterface) {
$buildable_phid = $adapter->getHarbormasterBuildablePHID();
HarbormasterBuildable::applyBuildPlans(
$buildable_phid,
$adapter->getHarbormasterContainerPHID(),
$adapter->getQueuedHarbormasterBuildRequests());
// Whether we queued any builds or not, any automatic buildable for this
// object is now done preparing builds and can transition into a
// completed status.
$buildables = id(new HarbormasterBuildableQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withManualBuildables(false)
->withBuildablePHIDs(array($buildable_phid))
->execute();
foreach ($buildables as $buildable) {
// If this buildable has already moved beyond preparation, we don't
// need to nudge it again.
if (!$buildable->isPreparing()) {
continue;
}
$buildable->sendMessage(
$this->getActor(),
HarbormasterMessageType::BUILDABLE_BUILD,
true);
}
}
$this->mustEncrypt = $adapter->getMustEncryptReasons();
+ // See PHI1134. Propagate "Must Encrypt" state to sub-editors.
+ foreach ($this->subEditors as $sub_editor) {
+ $sub_editor->mustEncrypt = $this->mustEncrypt;
+ }
+
+ $apply_xactions = $this->didApplyHeraldRules($object, $adapter, $xscript);
+ assert_instances_of($apply_xactions, 'PhabricatorApplicationTransaction');
+
+ $queue_xactions = $adapter->getQueuedTransactions();
+
return array_merge(
- $this->didApplyHeraldRules($object, $adapter, $xscript),
- $adapter->getQueuedTransactions());
+ array_values($apply_xactions),
+ array_values($queue_xactions));
}
protected function didApplyHeraldRules(
PhabricatorLiskDAO $object,
HeraldAdapter $adapter,
HeraldTranscript $transcript) {
return array();
}
/* -( Custom Fields )------------------------------------------------------ */
/**
* @task customfield
*/
private function getCustomFieldForTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$field_key = $xaction->getMetadataValue('customfield:key');
if (!$field_key) {
throw new Exception(
pht(
"Custom field transaction has no '%s'!",
'customfield:key'));
}
$field = PhabricatorCustomField::getObjectField(
$object,
PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS,
$field_key);
if (!$field) {
throw new Exception(
pht(
"Custom field transaction has invalid '%s'; field '%s' ".
"is disabled or does not exist.",
'customfield:key',
$field_key));
}
if (!$field->shouldAppearInApplicationTransactions()) {
throw new Exception(
pht(
"Custom field transaction '%s' does not implement ".
"integration for %s.",
$field_key,
'ApplicationTransactions'));
}
$field->setViewer($this->getActor());
return $field;
}
/* -( Files )-------------------------------------------------------------- */
/**
* Extract the PHIDs of any files which these transactions attach.
*
* @task files
*/
private function extractFilePHIDs(
PhabricatorLiskDAO $object,
array $xactions) {
$changes = $this->getRemarkupChanges($xactions);
$blocks = mpull($changes, 'getNewValue');
$phids = array();
if ($blocks) {
$phids[] = PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles(
$this->getActor(),
$blocks);
}
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$phids[] = $xtype->extractFilePHIDs($object, $xaction->getNewValue());
} else {
$phids[] = $this->extractFilePHIDsFromCustomTransaction(
$object,
$xaction);
}
}
$phids = array_unique(array_filter(array_mergev($phids)));
if (!$phids) {
return array();
}
// Only let a user attach files they can actually see, since this would
// otherwise let you access any file by attaching it to an object you have
// view permission on.
$files = id(new PhabricatorFileQuery())
->setViewer($this->getActor())
->withPHIDs($phids)
->execute();
return mpull($files, 'getPHID');
}
/**
* @task files
*/
protected function extractFilePHIDsFromCustomTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return array();
}
/**
* @task files
*/
private function attachFiles(
PhabricatorLiskDAO $object,
array $file_phids) {
if (!$file_phids) {
return;
}
$editor = new PhabricatorEdgeEditor();
$src = $object->getPHID();
$type = PhabricatorObjectHasFileEdgeType::EDGECONST;
foreach ($file_phids as $dst) {
$editor->addEdge($src, $type, $dst);
}
$editor->save();
}
private function applyInverseEdgeTransactions(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction,
$inverse_type) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$add = array_keys(array_diff_key($new, $old));
$rem = array_keys(array_diff_key($old, $new));
$add = array_fuse($add);
$rem = array_fuse($rem);
$all = $add + $rem;
$nodes = id(new PhabricatorObjectQuery())
->setViewer($this->requireActor())
->withPHIDs($all)
->execute();
$object_phid = $object->getPHID();
foreach ($nodes as $node) {
if (!($node instanceof PhabricatorApplicationTransactionInterface)) {
continue;
}
if ($node instanceof PhabricatorUser) {
// TODO: At least for now, don't record inverse edge transactions
// for users (for example, "alincoln joined project X"): Feed fills
// this role instead.
continue;
}
$node_phid = $node->getPHID();
$editor = $node->getApplicationTransactionEditor();
$template = $node->getApplicationTransactionTemplate();
// See T13082. We have to build these transactions with synthetic values
// because we've already applied the actual edit to the edge database
// table. If we try to apply this transaction naturally, it will no-op
// itself because it doesn't have any effect.
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($node_phid))
->withEdgeTypes(array($inverse_type));
$edge_query->execute();
$edge_phids = $edge_query->getDestinationPHIDs();
$edge_phids = array_fuse($edge_phids);
$new_phids = $edge_phids;
$old_phids = $edge_phids;
if (isset($add[$node_phid])) {
unset($old_phids[$object_phid]);
} else {
$old_phids[$object_phid] = $object_phid;
}
$template
->setTransactionType($xaction->getTransactionType())
->setMetadataValue('edge:type', $inverse_type)
->setOldValue($old_phids)
->setNewValue($new_phids);
- $editor
+ $editor = $this->newSubEditor($editor)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
- ->setParentMessageID($this->getParentMessageID())
- ->setIsInverseEdgeEditor(true)
- ->setIsSilent($this->getIsSilent())
- ->setActor($this->requireActor())
- ->setActingAsPHID($this->getActingAsPHID())
- ->setContentSource($this->getContentSource());
+ ->setIsInverseEdgeEditor(true);
$editor->applyTransactions($node, array($template));
}
}
/* -( Workers )------------------------------------------------------------ */
/**
* Load any object state which is required to publish transactions.
*
* This hook is invoked in the main process before we compute data related
* to publishing transactions (like email "To" and "CC" lists), and again in
* the worker before publishing occurs.
*
* @return object Publishable object.
* @task workers
*/
protected function willPublish(PhabricatorLiskDAO $object, array $xactions) {
return $object;
}
/**
* Convert the editor state to a serializable dictionary which can be passed
* to a worker.
*
* This data will be loaded with @{method:loadWorkerState} in the worker.
*
* @return dict<string, wild> Serializable editor state.
* @task workers
*/
final private function getWorkerState() {
$state = array();
foreach ($this->getAutomaticStateProperties() as $property) {
$state[$property] = $this->$property;
}
$custom_state = $this->getCustomWorkerState();
$custom_encoding = $this->getCustomWorkerStateEncoding();
$state += array(
'excludeMailRecipientPHIDs' => $this->getExcludeMailRecipientPHIDs(),
'custom' => $this->encodeStateForStorage($custom_state, $custom_encoding),
'custom.encoding' => $custom_encoding,
);
return $state;
}
/**
* Hook; return custom properties which need to be passed to workers.
*
* @return dict<string, wild> Custom properties.
* @task workers
*/
protected function getCustomWorkerState() {
return array();
}
/**
* Hook; return storage encoding for custom properties which need to be
* passed to workers.
*
* This primarily allows binary data to be passed to workers and survive
* JSON encoding.
*
* @return dict<string, string> Property encodings.
* @task workers
*/
protected function getCustomWorkerStateEncoding() {
return array();
}
/**
* Load editor state using a dictionary emitted by @{method:getWorkerState}.
*
* This method is used to load state when running worker operations.
*
* @param dict<string, wild> Editor state, from @{method:getWorkerState}.
* @return this
* @task workers
*/
final public function loadWorkerState(array $state) {
foreach ($this->getAutomaticStateProperties() as $property) {
$this->$property = idx($state, $property);
}
$exclude = idx($state, 'excludeMailRecipientPHIDs', array());
$this->setExcludeMailRecipientPHIDs($exclude);
$custom_state = idx($state, 'custom', array());
$custom_encodings = idx($state, 'custom.encoding', array());
$custom = $this->decodeStateFromStorage($custom_state, $custom_encodings);
$this->loadCustomWorkerState($custom);
return $this;
}
/**
* Hook; set custom properties on the editor from data emitted by
* @{method:getCustomWorkerState}.
*
* @param dict<string, wild> Custom state,
* from @{method:getCustomWorkerState}.
* @return this
* @task workers
*/
protected function loadCustomWorkerState(array $state) {
return $this;
}
/**
* Get a list of object properties which should be automatically sent to
* workers in the state data.
*
* These properties will be automatically stored and loaded by the editor in
* the worker.
*
* @return list<string> List of properties.
* @task workers
*/
private function getAutomaticStateProperties() {
return array(
'parentMessageID',
'isNewObject',
'heraldEmailPHIDs',
'heraldForcedEmailPHIDs',
'heraldHeader',
'mailToPHIDs',
'mailCCPHIDs',
'feedNotifyPHIDs',
'feedRelatedPHIDs',
'feedShouldPublish',
'mailShouldSend',
'mustEncrypt',
'mailStamps',
'mailUnexpandablePHIDs',
'mailMutedPHIDs',
'webhookMap',
'silent',
'sendHistory',
);
}
/**
* Apply encodings prior to storage.
*
* See @{method:getCustomWorkerStateEncoding}.
*
* @param map<string, wild> Map of values to encode.
* @param map<string, string> Map of encodings to apply.
* @return map<string, wild> Map of encoded values.
* @task workers
*/
final private function encodeStateForStorage(
array $state,
array $encodings) {
foreach ($state as $key => $value) {
$encoding = idx($encodings, $key);
switch ($encoding) {
case self::STORAGE_ENCODING_BINARY:
// The mechanics of this encoding (serialize + base64) are a little
// awkward, but it allows us encode arrays and still be JSON-safe
// with binary data.
$value = @serialize($value);
if ($value === false) {
throw new Exception(
pht(
'Failed to serialize() value for key "%s".',
$key));
}
$value = base64_encode($value);
if ($value === false) {
throw new Exception(
pht(
'Failed to base64 encode value for key "%s".',
$key));
}
break;
}
$state[$key] = $value;
}
return $state;
}
/**
* Undo storage encoding applied when storing state.
*
* See @{method:getCustomWorkerStateEncoding}.
*
* @param map<string, wild> Map of encoded values.
* @param map<string, string> Map of encodings.
* @return map<string, wild> Map of decoded values.
* @task workers
*/
final private function decodeStateFromStorage(
array $state,
array $encodings) {
foreach ($state as $key => $value) {
$encoding = idx($encodings, $key);
switch ($encoding) {
case self::STORAGE_ENCODING_BINARY:
$value = base64_decode($value);
if ($value === false) {
throw new Exception(
pht(
'Failed to base64_decode() value for key "%s".',
$key));
}
$value = unserialize($value);
break;
}
$state[$key] = $value;
}
return $state;
}
/**
* Remove conflicts from a list of projects.
*
* Objects aren't allowed to be tagged with multiple milestones in the same
* group, nor projects such that one tag is the ancestor of any other tag.
* If the list of PHIDs include mutually exclusive projects, remove the
* conflicting projects.
*
* @param list<phid> List of project PHIDs.
* @return list<phid> List with conflicts removed.
*/
private function applyProjectConflictRules(array $phids) {
if (!$phids) {
return array();
}
// Overall, the last project in the list wins in cases of conflict (so when
// you add something, the thing you just added sticks and removes older
// values).
// Beyond that, there are two basic cases:
// Milestones: An object can't be in "A > Sprint 3" and "A > Sprint 4".
// If multiple projects are milestones of the same parent, we only keep the
// last one.
// Ancestor: You can't be in "A" and "A > B". If "A > B" comes later
// in the list, we remove "A" and keep "A > B". If "A" comes later, we
// remove "A > B" and keep "A".
// Note that it's OK to be in "A > B" and "A > C". There's only a conflict
// if one project is an ancestor of another. It's OK to have something
// tagged with multiple projects which share a common ancestor, so long as
// they are not mutual ancestors.
$viewer = PhabricatorUser::getOmnipotentUser();
$projects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withPHIDs(array_keys($phids))
->execute();
$projects = mpull($projects, null, 'getPHID');
// We're going to build a map from each project with milestones to the last
// milestone in the list. This last milestone is the milestone we'll keep.
$milestone_map = array();
// We're going to build a set of the projects which have no descendants
// later in the list. This allows us to apply both ancestor rules.
$ancestor_map = array();
foreach ($phids as $phid => $ignored) {
$project = idx($projects, $phid);
if (!$project) {
continue;
}
// This is the last milestone we've seen, so set it as the selection for
// the project's parent. This might be setting a new value or overwriting
// an earlier value.
if ($project->isMilestone()) {
$parent_phid = $project->getParentProjectPHID();
$milestone_map[$parent_phid] = $phid;
}
// Since this is the last item in the list we've examined so far, add it
// to the set of projects with no later descendants.
$ancestor_map[$phid] = $phid;
// Remove any ancestors from the set, since this is a later descendant.
foreach ($project->getAncestorProjects() as $ancestor) {
$ancestor_phid = $ancestor->getPHID();
unset($ancestor_map[$ancestor_phid]);
}
}
// Now that we've built the maps, we can throw away all the projects which
// have conflicts.
foreach ($phids as $phid => $ignored) {
$project = idx($projects, $phid);
if (!$project) {
// If a PHID is invalid, we just leave it as-is. We could clean it up,
// but leaving it untouched is less likely to cause collateral damage.
continue;
}
// If this was a milestone, check if it was the last milestone from its
// group in the list. If not, remove it from the list.
if ($project->isMilestone()) {
$parent_phid = $project->getParentProjectPHID();
if ($milestone_map[$parent_phid] !== $phid) {
unset($phids[$phid]);
continue;
}
}
// If a later project in the list is a subproject of this one, it will
// have removed ancestors from the map. If this project does not point
// at itself in the ancestor map, it should be discarded in favor of a
// subproject that comes later.
if (idx($ancestor_map, $phid) !== $phid) {
unset($phids[$phid]);
continue;
}
// If a later project in the list is an ancestor of this one, it will
// have added itself to the map. If any ancestor of this project points
// at itself in the map, this project should be discarded in favor of
// that later ancestor.
foreach ($project->getAncestorProjects() as $ancestor) {
$ancestor_phid = $ancestor->getPHID();
if (isset($ancestor_map[$ancestor_phid])) {
unset($phids[$phid]);
continue 2;
}
}
}
return $phids;
}
/**
* When the view policy for an object is changed, scramble the secret keys
* for attached files to invalidate existing URIs.
*/
private function scrambleFileSecrets($object) {
// If this is a newly created object, we don't need to scramble anything
// since it couldn't have been previously published.
if ($this->getIsNewObject()) {
return;
}
// If the object is a file itself, scramble it.
if ($object instanceof PhabricatorFile) {
if ($this->shouldScramblePolicy($object->getViewPolicy())) {
$object->scrambleSecret();
$object->save();
}
}
$phid = $object->getPHID();
$attached_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$phid,
PhabricatorObjectHasFileEdgeType::EDGECONST);
if (!$attached_phids) {
return;
}
$omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
$files = id(new PhabricatorFileQuery())
->setViewer($omnipotent_viewer)
->withPHIDs($attached_phids)
->execute();
foreach ($files as $file) {
$view_policy = $file->getViewPolicy();
if ($this->shouldScramblePolicy($view_policy)) {
$file->scrambleSecret();
$file->save();
}
}
}
/**
* Check if a policy is strong enough to justify scrambling. Objects which
* are set to very open policies don't need to scramble their files, and
* files with very open policies don't need to be scrambled when associated
* objects change.
*/
private function shouldScramblePolicy($policy) {
switch ($policy) {
case PhabricatorPolicies::POLICY_PUBLIC:
case PhabricatorPolicies::POLICY_USER:
return false;
}
return true;
}
private function updateWorkboardColumns($object, $const, $old, $new) {
// If an object is removed from a project, remove it from any proxy
// columns for that project. This allows a task which is moved up from a
// milestone to the parent to move back into the "Backlog" column on the
// parent workboard.
if ($const != PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) {
return;
}
// TODO: This should likely be some future WorkboardInterface.
$appears_on_workboards = ($object instanceof ManiphestTask);
if (!$appears_on_workboards) {
return;
}
$removed_phids = array_keys(array_diff_key($old, $new));
if (!$removed_phids) {
return;
}
// Find any proxy columns for the removed projects.
$proxy_columns = id(new PhabricatorProjectColumnQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withProxyPHIDs($removed_phids)
->execute();
if (!$proxy_columns) {
return array();
}
$proxy_phids = mpull($proxy_columns, 'getPHID');
$position_table = new PhabricatorProjectColumnPosition();
$conn_w = $position_table->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE objectPHID = %s AND columnPHID IN (%Ls)',
$position_table->getTableName(),
$object->getPHID(),
$proxy_phids);
}
private function getModularTransactionTypes() {
if ($this->modularTypes === null) {
$template = $this->object->getApplicationTransactionTemplate();
if ($template instanceof PhabricatorModularTransaction) {
$xtypes = $template->newModularTransactionTypes();
foreach ($xtypes as $key => $xtype) {
$xtype = clone $xtype;
$xtype->setEditor($this);
$xtypes[$key] = $xtype;
}
} else {
$xtypes = array();
}
$this->modularTypes = $xtypes;
}
return $this->modularTypes;
}
private function getModularTransactionType($type) {
$types = $this->getModularTransactionTypes();
return idx($types, $type);
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this object.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created an object: %s.', $author, $object);
}
/* -( Queue )-------------------------------------------------------------- */
protected function queueTransaction(
PhabricatorApplicationTransaction $xaction) {
$this->transactionQueue[] = $xaction;
return $this;
}
private function flushTransactionQueue($object) {
if (!$this->transactionQueue) {
return;
}
$xactions = $this->transactionQueue;
$this->transactionQueue = array();
- $editor = $this->newQueueEditor();
+ $editor = $this->newEditorCopy();
return $editor->applyTransactions($object, $xactions);
}
- private function newQueueEditor() {
- $editor = id(newv(get_class($this), array()))
+ final protected function newSubEditor(
+ PhabricatorApplicationTransactionEditor $template = null) {
+ $editor = $this->newEditorCopy($template);
+
+ $editor->parentEditor = $this;
+ $this->subEditors[] = $editor;
+
+ return $editor;
+ }
+
+ private function newEditorCopy(
+ PhabricatorApplicationTransactionEditor $template = null) {
+ if ($template === null) {
+ $template = newv(get_class($this), array());
+ }
+
+ $editor = id(clone $template)
->setActor($this->getActor())
->setContentSource($this->getContentSource())
->setContinueOnNoEffect($this->getContinueOnNoEffect())
->setContinueOnMissingFields($this->getContinueOnMissingFields())
+ ->setParentMessageID($this->getParentMessageID())
->setIsSilent($this->getIsSilent());
if ($this->actingAsPHID !== null) {
$editor->setActingAsPHID($this->actingAsPHID);
}
+ $editor->mustEncrypt = $this->mustEncrypt;
+
return $editor;
}
/* -( Stamps )------------------------------------------------------------- */
public function newMailStampTemplates($object) {
$actor = $this->getActor();
$templates = array();
$extensions = $this->newMailExtensions($object);
foreach ($extensions as $extension) {
$stamps = $extension->newMailStampTemplates($object);
foreach ($stamps as $stamp) {
$key = $stamp->getKey();
if (isset($templates[$key])) {
throw new Exception(
pht(
'Mail extension ("%s") defines a stamp template with the '.
'same key ("%s") as another template. Each stamp template '.
'must have a unique key.',
get_class($extension),
$key));
}
$stamp->setViewer($actor);
$templates[$key] = $stamp;
}
}
return $templates;
}
final public function getMailStamp($key) {
if (!isset($this->stampTemplates)) {
throw new PhutilInvalidStateException('newMailStampTemplates');
}
if (!isset($this->stampTemplates[$key])) {
throw new Exception(
pht(
'Editor ("%s") has no mail stamp template with provided key ("%s").',
get_class($this),
$key));
}
return $this->stampTemplates[$key];
}
private function newMailStamps($object, array $xactions) {
$actor = $this->getActor();
$this->stampTemplates = $this->newMailStampTemplates($object);
$extensions = $this->newMailExtensions($object);
$stamps = array();
foreach ($extensions as $extension) {
$extension->newMailStamps($object, $xactions);
}
return $this->stampTemplates;
}
private function newMailExtensions($object) {
$actor = $this->getActor();
$all_extensions = PhabricatorMailEngineExtension::getAllExtensions();
$extensions = array();
foreach ($all_extensions as $key => $template) {
$extension = id(clone $template)
->setViewer($actor)
->setEditor($this);
if ($extension->supportsObject($object)) {
$extensions[$key] = $extension;
}
}
return $extensions;
}
private function generateMailStamps($object, $data) {
if (!$data || !is_array($data)) {
return null;
}
$templates = $this->newMailStampTemplates($object);
foreach ($data as $spec) {
if (!is_array($spec)) {
continue;
}
$key = idx($spec, 'key');
if (!isset($templates[$key])) {
continue;
}
$type = idx($spec, 'type');
if ($templates[$key]->getStampType() !== $type) {
continue;
}
$value = idx($spec, 'value');
$templates[$key]->setValueFromDictionary($value);
}
$results = array();
foreach ($templates as $template) {
$value = $template->getValueForRendering();
$rendered = $template->renderStamps($value);
if ($rendered === null) {
continue;
}
$rendered = (array)$rendered;
foreach ($rendered as $stamp) {
$results[] = $stamp;
}
}
natcasesort($results);
return $results;
}
public function getRemovedRecipientPHIDs() {
return $this->mailRemovedPHIDs;
}
private function buildOldRecipientLists($object, $xactions) {
// See T4776. Before we start making any changes, build a list of the old
// recipients. If a change removes a user from the recipient list for an
// object we still want to notify the user about that change. This allows
// them to respond if they didn't want to be removed.
if (!$this->shouldSendMail($object, $xactions)) {
return;
}
$this->oldTo = $this->getMailTo($object);
$this->oldCC = $this->getMailCC($object);
return $this;
}
private function applyOldRecipientLists() {
$actor_phid = $this->getActingAsPHID();
// If you took yourself off the recipient list (for example, by
// unsubscribing or resigning) assume that you know what you did and
// don't need to be notified.
// If you just moved from "To" to "Cc" (or vice versa), you're still a
// recipient so we don't need to add you back in.
$map = array_fuse($this->mailToPHIDs) + array_fuse($this->mailCCPHIDs);
foreach ($this->oldTo as $phid) {
if ($phid === $actor_phid) {
continue;
}
if (isset($map[$phid])) {
continue;
}
$this->mailToPHIDs[] = $phid;
$this->mailRemovedPHIDs[] = $phid;
}
foreach ($this->oldCC as $phid) {
if ($phid === $actor_phid) {
continue;
}
if (isset($map[$phid])) {
continue;
}
$this->mailCCPHIDs[] = $phid;
$this->mailRemovedPHIDs[] = $phid;
}
return $this;
}
private function queueWebhooks($object, array $xactions) {
$hook_viewer = PhabricatorUser::getOmnipotentUser();
$webhook_map = $this->webhookMap;
if (!is_array($webhook_map)) {
$webhook_map = array();
}
// Add any "Firehose" hooks to the list of hooks we're going to call.
$firehose_hooks = id(new HeraldWebhookQuery())
->setViewer($hook_viewer)
->withStatuses(
array(
HeraldWebhook::HOOKSTATUS_FIREHOSE,
))
->execute();
foreach ($firehose_hooks as $firehose_hook) {
// This is "the hook itself is the reason this hook is being called",
// since we're including it because it's configured as a firehose
// hook.
$hook_phid = $firehose_hook->getPHID();
$webhook_map[$hook_phid][] = $hook_phid;
}
if (!$webhook_map) {
return;
}
// NOTE: We're going to queue calls to disabled webhooks, they'll just
// immediately fail in the worker queue. This makes the behavior more
// visible.
$call_hooks = id(new HeraldWebhookQuery())
->setViewer($hook_viewer)
->withPHIDs(array_keys($webhook_map))
->execute();
foreach ($call_hooks as $call_hook) {
$trigger_phids = idx($webhook_map, $call_hook->getPHID());
$request = HeraldWebhookRequest::initializeNewWebhookRequest($call_hook)
->setObjectPHID($object->getPHID())
->setTransactionPHIDs(mpull($xactions, 'getPHID'))
->setTriggerPHIDs($trigger_phids)
->setRetryMode(HeraldWebhookRequest::RETRY_FOREVER)
->setIsSilentAction((bool)$this->getIsSilent())
->setIsSecureAction((bool)$this->getMustEncrypt())
->save();
$request->queueCall();
}
}
private function hasWarnings($object, $xaction) {
// TODO: For the moment, this is a very un-modular hack to support
// exactly one type of warning (mentioning users on a draft revision)
// that we want to show. See PHI433.
if (!($object instanceof DifferentialRevision)) {
return false;
}
if (!$object->isDraft()) {
return false;
}
$type = $xaction->getTransactionType();
if ($type != PhabricatorTransactions::TYPE_SUBSCRIBERS) {
return false;
}
// NOTE: This will currently warn even if you're only removing
// subscribers.
return true;
}
private function buildHistoryMail(PhabricatorLiskDAO $object) {
$viewer = $this->requireActor();
$recipient_phid = $this->getActingAsPHID();
// Load every transaction so we can build a mail message with a complete
// history for the object.
$query = PhabricatorApplicationTransactionQuery::newQueryForObject($object);
$xactions = $query
->setViewer($viewer)
->withObjectPHIDs(array($object->getPHID()))
->execute();
$xactions = array_reverse($xactions);
$mail_messages = $this->buildMailWithRecipients(
$object,
$xactions,
array($recipient_phid),
array(),
array());
$mail = head($mail_messages);
// Since the user explicitly requested "!history", force delivery of this
// message regardless of their other mail settings.
$mail->setForceDelivery(true);
return $mail;
}
public function newAutomaticInlineTransactions(
PhabricatorLiskDAO $object,
array $inlines,
$transaction_type,
PhabricatorCursorPagedPolicyAwareQuery $query_template) {
$xactions = array();
foreach ($inlines as $inline) {
$xactions[] = $object->getApplicationTransactionTemplate()
->setTransactionType($transaction_type)
->attachComment($inline);
}
$state_xaction = $this->newInlineStateTransaction(
$object,
$query_template);
if ($state_xaction) {
$xactions[] = $state_xaction;
}
return $xactions;
}
protected function newInlineStateTransaction(
PhabricatorLiskDAO $object,
PhabricatorCursorPagedPolicyAwareQuery $query_template) {
$actor_phid = $this->getActingAsPHID();
$author_phid = $object->getAuthorPHID();
$actor_is_author = ($actor_phid == $author_phid);
$state_map = PhabricatorTransactions::getInlineStateMap();
$query = id(clone $query_template)
->setViewer($this->getActor())
->withFixedStates(array_keys($state_map));
$inlines = array();
$inlines[] = id(clone $query)
->withAuthorPHIDs(array($actor_phid))
->withHasTransaction(false)
->execute();
if ($actor_is_author) {
$inlines[] = id(clone $query)
->withHasTransaction(true)
->execute();
}
$inlines = array_mergev($inlines);
if (!$inlines) {
return null;
}
$old_value = mpull($inlines, 'getFixedState', 'getPHID');
$new_value = array();
foreach ($old_value as $key => $state) {
$new_value[$key] = $state_map[$state];
}
// See PHI995. Copy some information about the inlines into the transaction
// so we can tailor rendering behavior. In particular, we don't want to
// render transactions about users marking their own inlines as "Done".
$inline_details = array();
foreach ($inlines as $inline) {
$inline_details[$inline->getPHID()] = array(
'authorPHID' => $inline->getAuthorPHID(),
);
}
return $object->getApplicationTransactionTemplate()
->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE)
->setIgnoreOnNoEffect(true)
->setMetadataValue('inline.details', $inline_details)
->setOldValue($old_value)
->setNewValue($new_value);
}
private function requireMFA(PhabricatorLiskDAO $object, array $xactions) {
$actor = $this->getActor();
// Let omnipotent editors skip MFA. This is mostly aimed at scripts.
if ($actor->isOmnipotent()) {
return;
}
$editor_class = get_class($this);
$object_phid = $object->getPHID();
if ($object_phid) {
$workflow_key = sprintf(
'editor(%s).phid(%s)',
$editor_class,
$object_phid);
} else {
$workflow_key = sprintf(
'editor(%s).new()',
$editor_class);
}
$request = $this->getRequest();
if ($request === null) {
$source_type = $this->getContentSource()->getSourceTypeConstant();
$conduit_type = PhabricatorConduitContentSource::SOURCECONST;
$is_conduit = ($source_type === $conduit_type);
if ($is_conduit) {
throw new Exception(
pht(
'This transaction group requires MFA to apply, but you can not '.
'provide an MFA response via Conduit. Edit this object via the '.
'web UI.'));
} else {
throw new Exception(
pht(
'This transaction group requires MFA to apply, but the Editor was '.
'not configured with a Request. This workflow can not perform an '.
'MFA check.'));
}
}
$cancel_uri = $this->getCancelURI();
if ($cancel_uri === null) {
throw new Exception(
pht(
'This transaction group requires MFA to apply, but the Editor was '.
'not configured with a Cancel URI. This workflow can not perform '.
'an MFA check.'));
}
id(new PhabricatorAuthSessionEngine())
->setWorkflowKey($workflow_key)
->requireHighSecurityToken($actor, $request, $cancel_uri);
foreach ($xactions as $xaction) {
$xaction->setIsMFATransaction(true);
}
}
private function newMFATransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$has_engine = ($object instanceof PhabricatorEditEngineMFAInterface);
if ($has_engine) {
$engine = PhabricatorEditEngineMFAEngine::newEngineForObject($object)
->setViewer($this->getActor());
$require_mfa = $engine->shouldRequireMFA();
$try_mfa = $engine->shouldTryMFA();
} else {
$require_mfa = false;
$try_mfa = false;
}
// If the user is mentioning an MFA object on another object or creating
// a relationship like "parent" or "child" to this object, we always
// allow the edit to move forward without requiring MFA.
if ($this->getIsInverseEdgeEditor()) {
return $xactions;
}
if (!$require_mfa) {
// If the object hasn't already opted into MFA, see if any of the
// transactions want it.
if (!$try_mfa) {
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
if ($xtype->shouldTryMFA($object, $xaction)) {
$try_mfa = true;
break;
}
}
}
}
if ($try_mfa) {
$this->setShouldRequireMFA(true);
}
return $xactions;
}
$type_mfa = PhabricatorTransactions::TYPE_MFA;
$has_mfa = false;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() === $type_mfa) {
$has_mfa = true;
break;
}
}
if ($has_mfa) {
return $xactions;
}
$template = $object->getApplicationTransactionTemplate();
$mfa_xaction = id(clone $template)
->setTransactionType($type_mfa)
->setNewValue(true);
array_unshift($xactions, $mfa_xaction);
return $xactions;
}
private function getTitleForTextMail(
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
$comment = $xtype->getTitleForTextMail();
if ($comment !== false) {
return $comment;
}
}
return $xaction->getTitleForTextMail();
}
private function getTitleForHTMLMail(
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
$comment = $xtype->getTitleForHTMLMail();
if ($comment !== false) {
return $comment;
}
}
return $xaction->getTitleForHTMLMail();
}
private function getBodyForTextMail(
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
$comment = $xtype->getBodyForTextMail();
if ($comment !== false) {
return $comment;
}
}
return $xaction->getBodyForMail();
}
/* -( Extensions )--------------------------------------------------------- */
private function validateTransactionsWithExtensions(
PhabricatorLiskDAO $object,
array $xactions) {
$errors = array();
$extensions = $this->getEditorExtensions();
foreach ($extensions as $extension) {
$extension_errors = $extension
->setObject($object)
->validateTransactions($object, $xactions);
assert_instances_of(
$extension_errors,
'PhabricatorApplicationTransactionValidationError');
$errors[] = $extension_errors;
}
return array_mergev($errors);
}
private function getEditorExtensions() {
if ($this->extensions === null) {
$this->extensions = $this->newEditorExtensions();
}
return $this->extensions;
}
private function newEditorExtensions() {
$extensions = PhabricatorEditorExtension::getAllExtensions();
$actor = $this->getActor();
$object = $this->object;
foreach ($extensions as $key => $extension) {
$extension = id(clone $extension)
->setViewer($actor)
->setEditor($this)
->setObject($object);
if (!$extension->supportsObject($this, $object)) {
unset($extensions[$key]);
continue;
}
$extensions[$key] = $extension;
}
return $extensions;
}
}
diff --git a/src/applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php b/src/applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php
index 0d2053379..7d80082cb 100644
--- a/src/applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php
+++ b/src/applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php
@@ -1,73 +1,68 @@
<?php
final class PhabricatorCommentEditEngineExtension
extends PhabricatorEditEngineExtension {
const EXTENSIONKEY = 'transactions.comment';
const EDITKEY = 'comment';
public function getExtensionPriority() {
return 9000;
}
public function isExtensionEnabled() {
return true;
}
public function getExtensionName() {
return pht('Comments');
}
public function supportsObject(
PhabricatorEditEngine $engine,
PhabricatorApplicationTransactionInterface $object) {
$xaction = $object->getApplicationTransactionTemplate();
-
- try {
- $comment = $xaction->getApplicationTransactionCommentObject();
- } catch (PhutilMethodNotImplementedException $ex) {
- $comment = null;
- }
+ $comment = $xaction->getApplicationTransactionCommentObject();
return (bool)$comment;
}
public function newBulkEditGroups(PhabricatorEditEngine $engine) {
return array(
id(new PhabricatorBulkEditGroup())
->setKey('comments')
->setLabel(pht('Comments')),
);
}
public function buildCustomEditFields(
PhabricatorEditEngine $engine,
PhabricatorApplicationTransactionInterface $object) {
$comment_type = PhabricatorTransactions::TYPE_COMMENT;
// Comments have a lot of special behavior which doesn't always check
// this flag, but we set it for consistency.
$is_interact = true;
$comment_field = id(new PhabricatorCommentEditField())
->setKey(self::EDITKEY)
->setLabel(pht('Comments'))
->setBulkEditLabel(pht('Add comment'))
->setBulkEditGroupKey('comments')
->setAliases(array('comments'))
->setIsFormField(false)
->setCanApplyWithoutEditCapability($is_interact)
->setTransactionType($comment_type)
->setConduitDescription(pht('Make comments.'))
->setConduitTypeDescription(
pht('Comment to add, formatted as remarkup.'))
->setValue(null);
return array(
$comment_field,
);
}
}
diff --git a/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php b/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php
index f15522a08..1db622163 100644
--- a/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php
+++ b/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php
@@ -1,252 +1,265 @@
<?php
abstract class PhabricatorApplicationTransactionQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
+ private $ids;
private $phids;
private $objectPHIDs;
private $authorPHIDs;
private $transactionTypes;
private $withComments;
private $needComments = true;
private $needHandles = true;
final public static function newQueryForObject(
PhabricatorApplicationTransactionInterface $object) {
$xaction = $object->getApplicationTransactionTemplate();
$target_class = get_class($xaction);
$queries = id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->execute();
foreach ($queries as $query) {
$query_xaction = $query->getTemplateApplicationTransaction();
$query_class = get_class($query_xaction);
if ($query_class === $target_class) {
return id(clone $query);
}
}
return null;
}
abstract public function getTemplateApplicationTransaction();
+ public function withIDs(array $ids) {
+ $this->ids = $ids;
+ return $this;
+ }
+
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withObjectPHIDs(array $object_phids) {
$this->objectPHIDs = $object_phids;
return $this;
}
public function withAuthorPHIDs(array $author_phids) {
$this->authorPHIDs = $author_phids;
return $this;
}
public function withTransactionTypes(array $transaction_types) {
$this->transactionTypes = $transaction_types;
return $this;
}
public function withComments($with_comments) {
$this->withComments = $with_comments;
return $this;
}
public function needComments($need) {
$this->needComments = $need;
return $this;
}
public function needHandles($need) {
$this->needHandles = $need;
return $this;
}
protected function loadPage() {
$table = $this->getTemplateApplicationTransaction();
$xactions = $this->loadStandardPage($table);
foreach ($xactions as $xaction) {
$xaction->attachViewer($this->getViewer());
}
if ($this->needComments) {
$comment_phids = array_filter(mpull($xactions, 'getCommentPHID'));
$comments = array();
if ($comment_phids) {
$comments =
id(new PhabricatorApplicationTransactionTemplatedCommentQuery())
->setTemplate($table->getApplicationTransactionCommentObject())
->setViewer($this->getViewer())
->withPHIDs($comment_phids)
->execute();
$comments = mpull($comments, null, 'getPHID');
}
foreach ($xactions as $xaction) {
if ($xaction->getCommentPHID()) {
$comment = idx($comments, $xaction->getCommentPHID());
if ($comment) {
$xaction->attachComment($comment);
}
}
}
} else {
foreach ($xactions as $xaction) {
$xaction->setCommentNotLoaded(true);
}
}
return $xactions;
}
protected function willFilterPage(array $xactions) {
$object_phids = array_keys(mpull($xactions, null, 'getObjectPHID'));
$objects = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs($object_phids)
->execute();
foreach ($xactions as $key => $xaction) {
$object_phid = $xaction->getObjectPHID();
if (empty($objects[$object_phid])) {
unset($xactions[$key]);
continue;
}
$xaction->attachObject($objects[$object_phid]);
}
// NOTE: We have to do this after loading objects, because the objects
// may help determine which handles are required (for example, in the case
// of custom fields).
if ($this->needHandles) {
$phids = array();
foreach ($xactions as $xaction) {
$phids[$xaction->getPHID()] = $xaction->getRequiredHandlePHIDs();
}
$handles = array();
$merged = array_mergev($phids);
if ($merged) {
$handles = $this->getViewer()->loadHandles($merged);
$handles = iterator_to_array($handles);
}
foreach ($xactions as $xaction) {
$xaction->setHandles(
array_select_keys(
$handles,
$phids[$xaction->getPHID()]));
}
}
return $xactions;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
+ if ($this->ids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'x.id IN (%Ld)',
+ $this->ids);
+ }
+
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'x.phid IN (%Ls)',
$this->phids);
}
if ($this->objectPHIDs !== null) {
$where[] = qsprintf(
$conn,
'x.objectPHID IN (%Ls)',
$this->objectPHIDs);
}
if ($this->authorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'x.authorPHID IN (%Ls)',
$this->authorPHIDs);
}
if ($this->transactionTypes !== null) {
$where[] = qsprintf(
$conn,
'x.transactionType IN (%Ls)',
$this->transactionTypes);
}
if ($this->withComments !== null) {
if (!$this->withComments) {
$where[] = qsprintf(
$conn,
'c.id IS NULL');
}
}
return $where;
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->withComments !== null) {
$xaction = $this->getTemplateApplicationTransaction();
$comment = $xaction->getApplicationTransactionCommentObject();
// Not every transaction type has comments, so we may be able to
// implement this constraint trivially.
if (!$comment) {
if ($this->withComments) {
throw new PhabricatorEmptyQueryException();
} else {
// If we're querying for transactions with no comments and the
// transaction type does not support comments, we don't need to
// do anything.
}
} else {
if ($this->withComments) {
$joins[] = qsprintf(
$conn,
'JOIN %T c ON x.phid = c.transactionPHID',
$comment->getTableName());
} else {
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T c ON x.phid = c.transactionPHID',
$comment->getTableName());
}
}
}
return $joins;
}
protected function shouldGroupQueryResultRows() {
if ($this->withComments !== null) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
public function getQueryApplicationClass() {
// TODO: Sort this out?
return null;
}
protected function getPrimaryTableAlias() {
return 'x';
}
}
diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
index 6d047fc82..d71728a01 100644
--- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
+++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
@@ -1,1755 +1,1764 @@
<?php
abstract class PhabricatorApplicationTransaction
extends PhabricatorLiskDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
const TARGET_TEXT = 'text';
const TARGET_HTML = 'html';
protected $phid;
protected $objectPHID;
protected $authorPHID;
protected $viewPolicy;
protected $editPolicy;
protected $commentPHID;
protected $commentVersion = 0;
protected $transactionType;
protected $oldValue;
protected $newValue;
protected $metadata = array();
protected $contentSource;
private $comment;
private $commentNotLoaded;
private $handles;
private $renderingTarget = self::TARGET_HTML;
private $transactionGroup = array();
private $viewer = self::ATTACHABLE;
private $object = self::ATTACHABLE;
private $oldValueHasBeenSet = false;
private $ignoreOnNoEffect;
/**
* Flag this transaction as a pure side-effect which should be ignored when
* applying transactions if it has no effect, even if transaction application
* would normally fail. This both provides users with better error messages
* and allows transactions to perform optional side effects.
*/
public function setIgnoreOnNoEffect($ignore) {
$this->ignoreOnNoEffect = $ignore;
return $this;
}
public function getIgnoreOnNoEffect() {
return $this->ignoreOnNoEffect;
}
public function shouldGenerateOldValue() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
case PhabricatorTransactions::TYPE_INLINESTATE:
return false;
}
return true;
}
abstract public function getApplicationTransactionType();
private function getApplicationObjectTypeName() {
$types = PhabricatorPHIDType::getAllTypes();
$type = idx($types, $this->getApplicationTransactionType());
if ($type) {
return $type->getTypeName();
}
return pht('Object');
}
public function getApplicationTransactionCommentObject() {
- throw new PhutilMethodNotImplementedException();
+ return null;
}
public function getMetadataValue($key, $default = null) {
return idx($this->metadata, $key, $default);
}
public function setMetadataValue($key, $value) {
$this->metadata[$key] = $value;
return $this;
}
public function generatePHID() {
$type = PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST;
$subtype = $this->getApplicationTransactionType();
return PhabricatorPHID::generateNewPHID($type, $subtype);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'oldValue' => self::SERIALIZATION_JSON,
'newValue' => self::SERIALIZATION_JSON,
'metadata' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'commentPHID' => 'phid?',
'commentVersion' => 'uint32',
'contentSource' => 'text',
'transactionType' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_object' => array(
'columns' => array('objectPHID'),
),
),
) + parent::getConfiguration();
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source->serialize();
return $this;
}
public function getContentSource() {
return PhabricatorContentSource::newFromSerialized($this->contentSource);
}
public function hasComment() {
return $this->getComment() && strlen($this->getComment()->getContent());
}
public function getComment() {
if ($this->commentNotLoaded) {
throw new Exception(pht('Comment for this transaction was not loaded.'));
}
return $this->comment;
}
public function setIsCreateTransaction($create) {
return $this->setMetadataValue('core.create', $create);
}
public function getIsCreateTransaction() {
return (bool)$this->getMetadataValue('core.create', false);
}
public function setIsDefaultTransaction($default) {
return $this->setMetadataValue('core.default', $default);
}
public function getIsDefaultTransaction() {
return (bool)$this->getMetadataValue('core.default', false);
}
public function setIsSilentTransaction($silent) {
return $this->setMetadataValue('core.silent', $silent);
}
public function getIsSilentTransaction() {
return (bool)$this->getMetadataValue('core.silent', false);
}
public function setIsMFATransaction($mfa) {
return $this->setMetadataValue('core.mfa', $mfa);
}
public function getIsMFATransaction() {
return (bool)$this->getMetadataValue('core.mfa', false);
}
+ public function setIsLockOverrideTransaction($override) {
+ return $this->setMetadataValue('core.lock-override', $override);
+ }
+
+ public function getIsLockOverrideTransaction() {
+ return (bool)$this->getMetadataValue('core.lock-override', false);
+ }
+
public function attachComment(
PhabricatorApplicationTransactionComment $comment) {
$this->comment = $comment;
$this->commentNotLoaded = false;
return $this;
}
public function setCommentNotLoaded($not_loaded) {
$this->commentNotLoaded = $not_loaded;
return $this;
}
public function attachObject($object) {
$this->object = $object;
return $this;
}
public function getObject() {
return $this->assertAttached($this->object);
}
public function getRemarkupChanges() {
$changes = $this->newRemarkupChanges();
assert_instances_of($changes, 'PhabricatorTransactionRemarkupChange');
// Convert older-style remarkup blocks into newer-style remarkup changes.
// This builds changes that do not have the correct "old value", so rules
// that operate differently against edits (like @user mentions) won't work
// properly.
foreach ($this->getRemarkupBlocks() as $block) {
$changes[] = $this->newRemarkupChange()
->setOldValue(null)
->setNewValue($block);
}
$comment = $this->getComment();
if ($comment) {
if ($comment->hasOldComment()) {
$old_value = $comment->getOldComment()->getContent();
} else {
$old_value = null;
}
$new_value = $comment->getContent();
$changes[] = $this->newRemarkupChange()
->setOldValue($old_value)
->setNewValue($new_value);
}
return $changes;
}
protected function newRemarkupChanges() {
return array();
}
protected function newRemarkupChange() {
return id(new PhabricatorTransactionRemarkupChange())
->setTransaction($this);
}
/**
* @deprecated
*/
public function getRemarkupBlocks() {
$blocks = array();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
$custom_blocks = $field->getApplicationTransactionRemarkupBlocks(
$this);
foreach ($custom_blocks as $custom_block) {
$blocks[] = $custom_block;
}
}
break;
}
return $blocks;
}
public function setOldValue($value) {
$this->oldValueHasBeenSet = true;
$this->writeField('oldValue', $value);
return $this;
}
public function hasOldValue() {
return $this->oldValueHasBeenSet;
}
public function newChronologicalSortVector() {
return id(new PhutilSortVector())
->addInt((int)$this->getDateCreated())
->addInt((int)$this->getID());
}
/* -( Rendering )---------------------------------------------------------- */
public function setRenderingTarget($rendering_target) {
$this->renderingTarget = $rendering_target;
return $this;
}
public function getRenderingTarget() {
return $this->renderingTarget;
}
public function attachViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->assertAttached($this->viewer);
}
public function getRequiredHandlePHIDs() {
$phids = array();
$old = $this->getOldValue();
$new = $this->getNewValue();
$phids[] = array($this->getAuthorPHID());
$phids[] = array($this->getObjectPHID());
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
$phids[] = $field->getApplicationTransactionRequiredHandlePHIDs(
$this);
}
break;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$phids[] = $old;
$phids[] = $new;
break;
case PhabricatorTransactions::TYPE_EDGE:
$record = PhabricatorEdgeChangeRecord::newFromTransaction($this);
$phids[] = $record->getChangedPHIDs();
break;
case PhabricatorTransactions::TYPE_COLUMNS:
foreach ($new as $move) {
$phids[] = array(
$move['columnPHID'],
$move['boardPHID'],
);
$phids[] = $move['fromColumnPHIDs'];
}
break;
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
if (!PhabricatorPolicyQuery::isSpecialPolicy($old)) {
$phids[] = array($old);
}
if (!PhabricatorPolicyQuery::isSpecialPolicy($new)) {
$phids[] = array($new);
}
break;
case PhabricatorTransactions::TYPE_SPACE:
if ($old) {
$phids[] = array($old);
}
if ($new) {
$phids[] = array($new);
}
break;
case PhabricatorTransactions::TYPE_TOKEN:
break;
}
if ($this->getComment()) {
$phids[] = array($this->getComment()->getAuthorPHID());
}
return array_mergev($phids);
}
public function setHandles(array $handles) {
$this->handles = $handles;
return $this;
}
public function getHandle($phid) {
if (empty($this->handles[$phid])) {
throw new Exception(
pht(
'Transaction ("%s", of type "%s") requires a handle ("%s") that it '.
'did not load.',
$this->getPHID(),
$this->getTransactionType(),
$phid));
}
return $this->handles[$phid];
}
public function getHandleIfExists($phid) {
return idx($this->handles, $phid);
}
public function getHandles() {
if ($this->handles === null) {
throw new Exception(
pht('Transaction requires handles and it did not load them.'));
}
return $this->handles;
}
public function renderHandleLink($phid) {
if ($this->renderingTarget == self::TARGET_HTML) {
return $this->getHandle($phid)->renderLink();
} else {
return $this->getHandle($phid)->getLinkName();
}
}
public function renderHandleList(array $phids) {
$links = array();
foreach ($phids as $phid) {
$links[] = $this->renderHandleLink($phid);
}
if ($this->renderingTarget == self::TARGET_HTML) {
return phutil_implode_html(', ', $links);
} else {
return implode(', ', $links);
}
}
private function renderSubscriberList(array $phids, $change_type) {
if ($this->getRenderingTarget() == self::TARGET_TEXT) {
return $this->renderHandleList($phids);
} else {
$handles = array_select_keys($this->getHandles(), $phids);
return id(new SubscriptionListStringBuilder())
->setHandles($handles)
->setObjectPHID($this->getPHID())
->buildTransactionString($change_type);
}
}
protected function renderPolicyName($phid, $state = 'old') {
$policy = PhabricatorPolicy::newFromPolicyAndHandle(
$phid,
$this->getHandleIfExists($phid));
if ($this->renderingTarget == self::TARGET_HTML) {
switch ($policy->getType()) {
case PhabricatorPolicyType::TYPE_CUSTOM:
$policy->setHref('/transactions/'.$state.'/'.$this->getPHID().'/');
$policy->setWorkflow(true);
break;
default:
break;
}
$output = $policy->renderDescription();
} else {
$output = hsprintf('%s', $policy->getFullName());
}
return $output;
}
public function getIcon() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$comment = $this->getComment();
if ($comment && $comment->getIsRemoved()) {
return 'fa-trash';
}
return 'fa-comment';
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$old = $this->getOldValue();
$new = $this->getNewValue();
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
if ($add && $rem) {
return 'fa-user';
} else if ($add) {
return 'fa-user-plus';
} else if ($rem) {
return 'fa-user-times';
} else {
return 'fa-user';
}
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
return 'fa-lock';
case PhabricatorTransactions::TYPE_EDGE:
switch ($this->getMetadataValue('edge:type')) {
case DiffusionCommitRevertedByCommitEdgeType::EDGECONST:
return 'fa-undo';
case DiffusionCommitRevertsCommitEdgeType::EDGECONST:
return 'fa-ambulance';
}
return 'fa-link';
case PhabricatorTransactions::TYPE_TOKEN:
return 'fa-trophy';
case PhabricatorTransactions::TYPE_SPACE:
return 'fa-th-large';
case PhabricatorTransactions::TYPE_COLUMNS:
return 'fa-columns';
case PhabricatorTransactions::TYPE_MFA:
return 'fa-vcard';
}
return 'fa-pencil';
}
public function getToken() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_TOKEN:
$old = $this->getOldValue();
$new = $this->getNewValue();
if ($new) {
$icon = substr($new, 10);
} else {
$icon = substr($old, 10);
}
return array($icon, !$this->getNewValue());
}
return array(null, null);
}
public function getColor() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT;
$comment = $this->getComment();
if ($comment && $comment->getIsRemoved()) {
return 'black';
}
break;
case PhabricatorTransactions::TYPE_EDGE:
switch ($this->getMetadataValue('edge:type')) {
case DiffusionCommitRevertedByCommitEdgeType::EDGECONST:
return 'pink';
case DiffusionCommitRevertsCommitEdgeType::EDGECONST:
return 'sky';
}
break;
case PhabricatorTransactions::TYPE_MFA;
return 'pink';
}
return null;
}
protected function getTransactionCustomField() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$key = $this->getMetadataValue('customfield:key');
if (!$key) {
return null;
}
$object = $this->getObject();
if (!($object instanceof PhabricatorCustomFieldInterface)) {
return null;
}
$field = PhabricatorCustomField::getObjectField(
$object,
PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS,
$key);
if (!$field) {
return null;
}
$field->setViewer($this->getViewer());
return $field;
}
return null;
}
public function shouldHide() {
// Never hide comments.
if ($this->hasComment()) {
return false;
}
$xaction_type = $this->getTransactionType();
// Always hide requests for object history.
if ($xaction_type === PhabricatorTransactions::TYPE_HISTORY) {
return true;
}
// Hide creation transactions if the old value is empty. These are
// transactions like "alice set the task title to: ...", which are
// essentially never interesting.
if ($this->getIsCreateTransaction()) {
switch ($xaction_type) {
case PhabricatorTransactions::TYPE_CREATE:
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
case PhabricatorTransactions::TYPE_SPACE:
break;
case PhabricatorTransactions::TYPE_SUBTYPE:
return true;
default:
$old = $this->getOldValue();
if (is_array($old) && !$old) {
return true;
}
if (!is_array($old)) {
if (!strlen($old)) {
return true;
}
// The integer 0 is also uninteresting by default; this is often
// an "off" flag for something like "All Day Event".
if ($old === 0) {
return true;
}
}
break;
}
}
// Hide creation transactions setting values to defaults, even if
// the old value is not empty. For example, tasks may have a global
// default view policy of "All Users", but a particular form sets the
// policy to "Administrators". The transaction corresponding to this
// change is not interesting, since it is the default behavior of the
// form.
if ($this->getIsCreateTransaction()) {
if ($this->getIsDefaultTransaction()) {
return true;
}
}
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
case PhabricatorTransactions::TYPE_SPACE:
if ($this->getIsCreateTransaction()) {
break;
}
// TODO: Remove this eventually, this is handling old changes during
// object creation prior to the introduction of "create" and "default"
// transaction display flags.
// NOTE: We can also hit this case with Space transactions that later
// update a default space (`null`) to an explicit space, so handling
// the Space case may require some finesse.
if ($this->getOldValue() === null) {
return true;
} else {
return false;
}
break;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
return $field->shouldHideInApplicationTransactions($this);
}
break;
case PhabricatorTransactions::TYPE_COLUMNS:
return !$this->getInterestingMoves($this->getNewValue());
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $this->getMetadataValue('edge:type');
switch ($edge_type) {
case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
case ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST:
case ManiphestTaskIsDuplicateOfTaskEdgeType::EDGECONST:
case PhabricatorMutedEdgeType::EDGECONST:
case PhabricatorMutedByEdgeType::EDGECONST:
return true;
break;
case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
$record = PhabricatorEdgeChangeRecord::newFromTransaction($this);
$add = $record->getAddedPHIDs();
$add_value = reset($add);
$add_handle = $this->getHandle($add_value);
if ($add_handle->getPolicyFiltered()) {
return true;
}
return false;
break;
default:
break;
}
break;
case PhabricatorTransactions::TYPE_INLINESTATE:
list($done, $undone) = $this->getInterestingInlineStateChangeCounts();
if (!$done && !$undone) {
return true;
}
break;
}
return false;
}
public function shouldHideForMail(array $xactions) {
if ($this->isSelfSubscription()) {
return true;
}
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_TOKEN:
return true;
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $this->getMetadataValue('edge:type');
switch ($edge_type) {
case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
return true;
case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST:
// When an object is first created, we hide any corresponding
// project transactions in the web UI because you can just look at
// the UI element elsewhere on screen to see which projects it
// is tagged with. However, in mail there's no other way to get
// this information, and it has some amount of value to users, so
// we keep the transaction. See T10493.
return false;
default:
break;
}
break;
}
if ($this->isInlineCommentTransaction()) {
$inlines = array();
// If there's a normal comment, we don't need to publish the inline
// transaction, since the normal comment covers things.
foreach ($xactions as $xaction) {
if ($xaction->isInlineCommentTransaction()) {
$inlines[] = $xaction;
continue;
}
// We found a normal comment, so hide this inline transaction.
if ($xaction->hasComment()) {
return true;
}
}
// If there are several inline comments, only publish the first one.
if ($this !== head($inlines)) {
return true;
}
}
return $this->shouldHide();
}
public function shouldHideForFeed() {
if ($this->isSelfSubscription()) {
return true;
}
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_MFA:
return true;
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $this->getMetadataValue('edge:type');
switch ($edge_type) {
case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
return true;
break;
default:
break;
}
break;
case PhabricatorTransactions::TYPE_INLINESTATE:
return true;
}
return $this->shouldHide();
}
public function shouldHideForNotifications() {
return $this->shouldHideForFeed();
}
private function getTitleForMailWithRenderingTarget($new_target) {
$old_target = $this->getRenderingTarget();
try {
$this->setRenderingTarget($new_target);
$result = $this->getTitleForMail();
} catch (Exception $ex) {
$this->setRenderingTarget($old_target);
throw $ex;
}
$this->setRenderingTarget($old_target);
return $result;
}
public function getTitleForMail() {
return $this->getTitle();
}
public function getTitleForTextMail() {
return $this->getTitleForMailWithRenderingTarget(self::TARGET_TEXT);
}
public function getTitleForHTMLMail() {
// TODO: For now, rendering this with TARGET_HTML generates links with
// bad targets ("/x/y/" instead of "https://dev.example.com/x/y/"). Throw
// a rug over the issue for the moment. See T12921.
$title = $this->getTitleForMailWithRenderingTarget(self::TARGET_TEXT);
if ($title === null) {
return null;
}
if ($this->hasChangeDetails()) {
$details_uri = $this->getChangeDetailsURI();
$details_uri = PhabricatorEnv::getProductionURI($details_uri);
$show_details = phutil_tag(
'a',
array(
'href' => $details_uri,
),
pht('(Show Details)'));
$title = array($title, ' ', $show_details);
}
return $title;
}
public function getChangeDetailsURI() {
return '/transactions/detail/'.$this->getPHID().'/';
}
public function getBodyForMail() {
if ($this->isInlineCommentTransaction()) {
// We don't return inline comment content as mail body content, because
// applications need to contextualize it (by adding line numbers, for
// example) in order for it to make sense.
return null;
}
$comment = $this->getComment();
if ($comment && strlen($comment->getContent())) {
return $comment->getContent();
}
return null;
}
public function getNoEffectDescription() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return pht('You can not post an empty comment.');
case PhabricatorTransactions::TYPE_VIEW_POLICY:
return pht(
'This %s already has that view policy.',
$this->getApplicationObjectTypeName());
case PhabricatorTransactions::TYPE_EDIT_POLICY:
return pht(
'This %s already has that edit policy.',
$this->getApplicationObjectTypeName());
case PhabricatorTransactions::TYPE_JOIN_POLICY:
return pht(
'This %s already has that join policy.',
$this->getApplicationObjectTypeName());
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return pht(
'All users are already subscribed to this %s.',
$this->getApplicationObjectTypeName());
case PhabricatorTransactions::TYPE_SPACE:
return pht('This object is already in that space.');
case PhabricatorTransactions::TYPE_EDGE:
return pht('Edges already exist; transaction has no effect.');
case PhabricatorTransactions::TYPE_COLUMNS:
return pht(
'You have not moved this object to any columns it is not '.
'already in.');
case PhabricatorTransactions::TYPE_MFA:
return pht(
'You can not sign a transaction group that has no other '.
'effects.');
}
return pht(
'Transaction (of type "%s") has no effect.',
$this->getTransactionType());
}
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CREATE:
return pht(
'%s created this object.',
$this->renderHandleLink($author_phid));
case PhabricatorTransactions::TYPE_COMMENT:
return pht(
'%s added a comment.',
$this->renderHandleLink($author_phid));
case PhabricatorTransactions::TYPE_VIEW_POLICY:
if ($this->getIsCreateTransaction()) {
return pht(
'%s created this object with visibility "%s".',
$this->renderHandleLink($author_phid),
$this->renderPolicyName($new, 'new'));
} else {
return pht(
'%s changed the visibility from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->renderPolicyName($old, 'old'),
$this->renderPolicyName($new, 'new'));
}
case PhabricatorTransactions::TYPE_EDIT_POLICY:
if ($this->getIsCreateTransaction()) {
return pht(
'%s created this object with edit policy "%s".',
$this->renderHandleLink($author_phid),
$this->renderPolicyName($new, 'new'));
} else {
return pht(
'%s changed the edit policy from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->renderPolicyName($old, 'old'),
$this->renderPolicyName($new, 'new'));
}
case PhabricatorTransactions::TYPE_JOIN_POLICY:
if ($this->getIsCreateTransaction()) {
return pht(
'%s created this object with join policy "%s".',
$this->renderHandleLink($author_phid),
$this->renderPolicyName($new, 'new'));
} else {
return pht(
'%s changed the join policy from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->renderPolicyName($old, 'old'),
$this->renderPolicyName($new, 'new'));
}
case PhabricatorTransactions::TYPE_SPACE:
if ($this->getIsCreateTransaction()) {
return pht(
'%s created this object in space %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($new));
} else {
return pht(
'%s shifted this object from the %s space to the %s space.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($old),
$this->renderHandleLink($new));
}
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
if ($add && $rem) {
return pht(
'%s edited subscriber(s), added %d: %s; removed %d: %s.',
$this->renderHandleLink($author_phid),
count($add),
$this->renderSubscriberList($add, 'add'),
count($rem),
$this->renderSubscriberList($rem, 'rem'));
} else if ($add) {
return pht(
'%s added %d subscriber(s): %s.',
$this->renderHandleLink($author_phid),
count($add),
$this->renderSubscriberList($add, 'add'));
} else if ($rem) {
return pht(
'%s removed %d subscriber(s): %s.',
$this->renderHandleLink($author_phid),
count($rem),
$this->renderSubscriberList($rem, 'rem'));
} else {
// This is used when rendering previews, before the user actually
// selects any CCs.
return pht(
'%s updated subscribers...',
$this->renderHandleLink($author_phid));
}
break;
case PhabricatorTransactions::TYPE_EDGE:
$record = PhabricatorEdgeChangeRecord::newFromTransaction($this);
$add = $record->getAddedPHIDs();
$rem = $record->getRemovedPHIDs();
$type = $this->getMetadata('edge:type');
$type = head($type);
try {
$type_obj = PhabricatorEdgeType::getByConstant($type);
} catch (Exception $ex) {
// Recover somewhat gracefully from edge transactions which
// we don't have the classes for.
return pht(
'%s edited an edge.',
$this->renderHandleLink($author_phid));
}
if ($add && $rem) {
return $type_obj->getTransactionEditString(
$this->renderHandleLink($author_phid),
new PhutilNumber(count($add) + count($rem)),
phutil_count($add),
$this->renderHandleList($add),
phutil_count($rem),
$this->renderHandleList($rem));
} else if ($add) {
return $type_obj->getTransactionAddString(
$this->renderHandleLink($author_phid),
phutil_count($add),
$this->renderHandleList($add));
} else if ($rem) {
return $type_obj->getTransactionRemoveString(
$this->renderHandleLink($author_phid),
phutil_count($rem),
$this->renderHandleList($rem));
} else {
return $type_obj->getTransactionPreviewString(
$this->renderHandleLink($author_phid));
}
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
return $field->getApplicationTransactionTitle($this);
} else {
$developer_mode = 'phabricator.developer-mode';
$is_developer = PhabricatorEnv::getEnvConfig($developer_mode);
if ($is_developer) {
return pht(
'%s edited a custom field (with key "%s").',
$this->renderHandleLink($author_phid),
$this->getMetadata('customfield:key'));
} else {
return pht(
'%s edited a custom field.',
$this->renderHandleLink($author_phid));
}
}
case PhabricatorTransactions::TYPE_TOKEN:
if ($old && $new) {
return pht(
'%s updated a token.',
$this->renderHandleLink($author_phid));
} else if ($old) {
return pht(
'%s rescinded a token.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s awarded a token.',
$this->renderHandleLink($author_phid));
}
case PhabricatorTransactions::TYPE_INLINESTATE:
list($done, $undone) = $this->getInterestingInlineStateChangeCounts();
if ($done && $undone) {
return pht(
'%s marked %s inline comment(s) as done and %s inline comment(s) '.
'as not done.',
$this->renderHandleLink($author_phid),
new PhutilNumber($done),
new PhutilNumber($undone));
} else if ($done) {
return pht(
'%s marked %s inline comment(s) as done.',
$this->renderHandleLink($author_phid),
new PhutilNumber($done));
} else {
return pht(
'%s marked %s inline comment(s) as not done.',
$this->renderHandleLink($author_phid),
new PhutilNumber($undone));
}
break;
case PhabricatorTransactions::TYPE_COLUMNS:
$moves = $this->getInterestingMoves($new);
if (count($moves) == 1) {
$move = head($moves);
$from_columns = $move['fromColumnPHIDs'];
$to_column = $move['columnPHID'];
$board_phid = $move['boardPHID'];
if (count($from_columns) == 1) {
return pht(
'%s moved this task from %s to %s on the %s board.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink(head($from_columns)),
$this->renderHandleLink($to_column),
$this->renderHandleLink($board_phid));
} else {
return pht(
'%s moved this task to %s on the %s board.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($to_column),
$this->renderHandleLink($board_phid));
}
} else {
$fragments = array();
foreach ($moves as $move) {
$fragments[] = pht(
'%s (%s)',
$this->renderHandleLink($board_phid),
$this->renderHandleLink($to_column));
}
return pht(
'%s moved this task on %s board(s): %s.',
$this->renderHandleLink($author_phid),
phutil_count($moves),
phutil_implode_html(', ', $fragments));
}
break;
case PhabricatorTransactions::TYPE_MFA:
return pht(
'%s signed these changes with MFA.',
$this->renderHandleLink($author_phid));
default:
// In developer mode, provide a better hint here about which string
// we're missing.
$developer_mode = 'phabricator.developer-mode';
$is_developer = PhabricatorEnv::getEnvConfig($developer_mode);
if ($is_developer) {
return pht(
'%s edited this object (transaction type "%s").',
$this->renderHandleLink($author_phid),
$this->getTransactionType());
} else {
return pht(
'%s edited this %s.',
$this->renderHandleLink($author_phid),
$this->getApplicationObjectTypeName());
}
}
}
public function getTitleForFeed() {
$author_phid = $this->getAuthorPHID();
$object_phid = $this->getObjectPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CREATE:
return pht(
'%s created %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_COMMENT:
return pht(
'%s added a comment to %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_VIEW_POLICY:
return pht(
'%s changed the visibility for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_EDIT_POLICY:
return pht(
'%s changed the edit policy for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_JOIN_POLICY:
return pht(
'%s changed the join policy for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return pht(
'%s updated subscribers of %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_SPACE:
if ($this->getIsCreateTransaction()) {
return pht(
'%s created %s in the %s space.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$this->renderHandleLink($new));
} else {
return pht(
'%s shifted %s from the %s space to the %s space.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$this->renderHandleLink($old),
$this->renderHandleLink($new));
}
case PhabricatorTransactions::TYPE_EDGE:
$record = PhabricatorEdgeChangeRecord::newFromTransaction($this);
$add = $record->getAddedPHIDs();
$rem = $record->getRemovedPHIDs();
$type = $this->getMetadata('edge:type');
$type = head($type);
$type_obj = PhabricatorEdgeType::getByConstant($type);
if ($add && $rem) {
return $type_obj->getFeedEditString(
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
new PhutilNumber(count($add) + count($rem)),
phutil_count($add),
$this->renderHandleList($add),
phutil_count($rem),
$this->renderHandleList($rem));
} else if ($add) {
return $type_obj->getFeedAddString(
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
phutil_count($add),
$this->renderHandleList($add));
} else if ($rem) {
return $type_obj->getFeedRemoveString(
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
phutil_count($rem),
$this->renderHandleList($rem));
} else {
return pht(
'%s edited edge metadata for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
}
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
return $field->getApplicationTransactionTitleForFeed($this);
} else {
return pht(
'%s edited a custom field on %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
}
case PhabricatorTransactions::TYPE_COLUMNS:
$moves = $this->getInterestingMoves($new);
if (count($moves) == 1) {
$move = head($moves);
$from_columns = $move['fromColumnPHIDs'];
$to_column = $move['columnPHID'];
$board_phid = $move['boardPHID'];
if (count($from_columns) == 1) {
return pht(
'%s moved %s from %s to %s on the %s board.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$this->renderHandleLink(head($from_columns)),
$this->renderHandleLink($to_column),
$this->renderHandleLink($board_phid));
} else {
return pht(
'%s moved %s to %s on the %s board.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$this->renderHandleLink($to_column),
$this->renderHandleLink($board_phid));
}
} else {
$fragments = array();
foreach ($moves as $move) {
$fragments[] = pht(
'%s (%s)',
$this->renderHandleLink($board_phid),
$this->renderHandleLink($to_column));
}
return pht(
'%s moved %s on %s board(s): %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
phutil_count($moves),
phutil_implode_html(', ', $fragments));
}
break;
case PhabricatorTransactions::TYPE_MFA:
return null;
}
return $this->getTitle();
}
public function getMarkupFieldsForFeed(PhabricatorFeedStory $story) {
$fields = array();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$text = $this->getComment()->getContent();
if (strlen($text)) {
$fields[] = 'comment/'.$this->getID();
}
break;
}
return $fields;
}
public function getMarkupTextForFeed(PhabricatorFeedStory $story, $field) {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$text = $this->getComment()->getContent();
return PhabricatorMarkupEngine::summarize($text);
}
return null;
}
public function getBodyForFeed(PhabricatorFeedStory $story) {
$remarkup = $this->getRemarkupBodyForFeed($story);
if ($remarkup !== null) {
$remarkup = PhabricatorMarkupEngine::summarize($remarkup);
return new PHUIRemarkupView($this->viewer, $remarkup);
}
$old = $this->getOldValue();
$new = $this->getNewValue();
$body = null;
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$text = $this->getComment()->getContent();
if (strlen($text)) {
$body = $story->getMarkupFieldOutput('comment/'.$this->getID());
}
break;
}
return $body;
}
public function getRemarkupBodyForFeed(PhabricatorFeedStory $story) {
return null;
}
public function getActionStrength() {
if ($this->isInlineCommentTransaction()) {
return 0.25;
}
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return 0.5;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
if ($this->isSelfSubscription()) {
// Make this weaker than TYPE_COMMENT.
return 0.25;
}
if ($this->isApplicationAuthor()) {
// When applications (most often: Herald) change subscriptions it
// is very uninteresting.
return 0.000000001;
}
// In other cases, subscriptions are more interesting than comments
// (which are shown anyway) but less interesting than any other type of
// transaction.
return 0.75;
case PhabricatorTransactions::TYPE_MFA:
// We want MFA signatures to render at the top of transaction groups,
// on top of the things they signed.
return 10;
}
return 1.0;
}
public function isCommentTransaction() {
if ($this->hasComment()) {
return true;
}
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return true;
}
return false;
}
public function isInlineCommentTransaction() {
return false;
}
public function getActionName() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return pht('Commented On');
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
return pht('Changed Policy');
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return pht('Changed Subscribers');
default:
return pht('Updated');
}
}
public function getMailTags() {
return array();
}
public function hasChangeDetails() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
return $field->getApplicationTransactionHasChangeDetails($this);
}
break;
}
return false;
}
public function hasChangeDetailsForMail() {
return $this->hasChangeDetails();
}
public function renderChangeDetailsForMail(PhabricatorUser $viewer) {
$view = $this->renderChangeDetails($viewer);
if ($view instanceof PhabricatorApplicationTransactionTextDiffDetailView) {
return $view->renderForMail();
}
return null;
}
public function renderChangeDetails(PhabricatorUser $viewer) {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
return $field->getApplicationTransactionChangeDetails($this, $viewer);
}
break;
}
return $this->renderTextCorpusChangeDetails(
$viewer,
$this->getOldValue(),
$this->getNewValue());
}
public function renderTextCorpusChangeDetails(
PhabricatorUser $viewer,
$old,
$new) {
return id(new PhabricatorApplicationTransactionTextDiffDetailView())
->setUser($viewer)
->setOldText($old)
->setNewText($new);
}
public function attachTransactionGroup(array $group) {
assert_instances_of($group, __CLASS__);
$this->transactionGroup = $group;
return $this;
}
public function getTransactionGroup() {
return $this->transactionGroup;
}
/**
* Should this transaction be visually grouped with an existing transaction
* group?
*
* @param list<PhabricatorApplicationTransaction> List of transactions.
* @return bool True to display in a group with the other transactions.
*/
public function shouldDisplayGroupWith(array $group) {
$this_source = null;
if ($this->getContentSource()) {
$this_source = $this->getContentSource()->getSource();
}
$type_mfa = PhabricatorTransactions::TYPE_MFA;
foreach ($group as $xaction) {
// Don't group transactions by different authors.
if ($xaction->getAuthorPHID() != $this->getAuthorPHID()) {
return false;
}
// Don't group transactions for different objects.
if ($xaction->getObjectPHID() != $this->getObjectPHID()) {
return false;
}
// Don't group anything into a group which already has a comment.
if ($xaction->isCommentTransaction()) {
return false;
}
// Don't group transactions from different content sources.
$other_source = null;
if ($xaction->getContentSource()) {
$other_source = $xaction->getContentSource()->getSource();
}
if ($other_source != $this_source) {
return false;
}
// Don't group transactions which happened more than 2 minutes apart.
$apart = abs($xaction->getDateCreated() - $this->getDateCreated());
if ($apart > (60 * 2)) {
return false;
}
// Don't group silent and nonsilent transactions together.
$is_silent = $this->getIsSilentTransaction();
if ($is_silent != $xaction->getIsSilentTransaction()) {
return false;
}
// Don't group MFA and non-MFA transactions together.
$is_mfa = $this->getIsMFATransaction();
if ($is_mfa != $xaction->getIsMFATransaction()) {
return false;
}
// Don't group two "Sign with MFA" transactions together.
if ($this->getTransactionType() === $type_mfa) {
if ($xaction->getTransactionType() === $type_mfa) {
return false;
}
}
+
+ // Don't group lock override and non-override transactions together.
+ $is_override = $this->getIsLockOverrideTransaction();
+ if ($is_override != $xaction->getIsLockOverrideTransaction()) {
+ return false;
+ }
}
return true;
}
public function renderExtraInformationLink() {
$herald_xscript_id = $this->getMetadataValue('herald:transcriptID');
if ($herald_xscript_id) {
return phutil_tag(
'a',
array(
'href' => '/herald/transcript/'.$herald_xscript_id.'/',
),
pht('View Herald Transcript'));
}
return null;
}
public function renderAsTextForDoorkeeper(
DoorkeeperFeedStoryPublisher $publisher,
PhabricatorFeedStory $story,
array $xactions) {
$text = array();
$body = array();
foreach ($xactions as $xaction) {
$xaction_body = $xaction->getBodyForMail();
if ($xaction_body !== null) {
$body[] = $xaction_body;
}
if ($xaction->shouldHideForMail($xactions)) {
continue;
}
$old_target = $xaction->getRenderingTarget();
$new_target = self::TARGET_TEXT;
$xaction->setRenderingTarget($new_target);
if ($publisher->getRenderWithImpliedContext()) {
$text[] = $xaction->getTitle();
} else {
$text[] = $xaction->getTitleForFeed();
}
$xaction->setRenderingTarget($old_target);
}
$text = implode("\n", $text);
$body = implode("\n\n", $body);
return rtrim($text."\n\n".$body);
}
/**
* Test if this transaction is just a user subscribing or unsubscribing
* themselves.
*/
private function isSelfSubscription() {
$type = $this->getTransactionType();
if ($type != PhabricatorTransactions::TYPE_SUBSCRIBERS) {
return false;
}
$old = $this->getOldValue();
$new = $this->getNewValue();
$add = array_diff($old, $new);
$rem = array_diff($new, $old);
if ((count($add) + count($rem)) != 1) {
// More than one user affected.
return false;
}
$affected_phid = head(array_merge($add, $rem));
if ($affected_phid != $this->getAuthorPHID()) {
// Affected user is someone else.
return false;
}
return true;
}
private function isApplicationAuthor() {
$author_phid = $this->getAuthorPHID();
$author_type = phid_get_type($author_phid);
$application_type = PhabricatorApplicationApplicationPHIDType::TYPECONST;
return ($author_type == $application_type);
}
private function getInterestingMoves(array $moves) {
// Remove moves which only shift the position of a task within a column.
foreach ($moves as $key => $move) {
$from_phids = array_fuse($move['fromColumnPHIDs']);
if (isset($from_phids[$move['columnPHID']])) {
unset($moves[$key]);
}
}
return $moves;
}
private function getInterestingInlineStateChangeCounts() {
// See PHI995. Newer inline state transactions have additional details
// which we use to tailor the rendering behavior. These details are not
// present on older transactions.
$details = $this->getMetadataValue('inline.details', array());
$new = $this->getNewValue();
$done = 0;
$undone = 0;
foreach ($new as $phid => $state) {
$is_done = ($state == PhabricatorInlineCommentInterface::STATE_DONE);
// See PHI995. If you're marking your own inline comments as "Done",
// don't count them when rendering a timeline story. In the case where
// you're only affecting your own comments, this will hide the
// "alice marked X comments as done" story entirely.
// Usually, this happens when you pre-mark inlines as "done" and submit
// them yourself. We'll still generate an "alice added inline comments"
// story (in most cases/contexts), but the state change story is largely
// just clutter and slightly confusing/misleading.
$inline_details = idx($details, $phid, array());
$inline_author_phid = idx($inline_details, 'authorPHID');
if ($inline_author_phid) {
if ($inline_author_phid == $this->getAuthorPHID()) {
if ($is_done) {
continue;
}
}
}
if ($is_done) {
$done++;
} else {
$undone++;
}
}
return array($done, $undone);
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return ($viewer->getPHID() == $this->getAuthorPHID());
}
public function describeAutomaticCapability($capability) {
return pht(
'Transactions are visible to users that can see the object which was '.
'acted upon. Some transactions - in particular, comments - are '.
'editable by the transaction author.');
}
public function getModularType() {
return null;
}
public function setForceNotifyPHIDs(array $phids) {
$this->setMetadataValue('notify.force', $phids);
return $this;
}
public function getForceNotifyPHIDs() {
return $this->getMetadataValue('notify.force', array());
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
- $comment_template = null;
- try {
- $comment_template = $this->getApplicationTransactionCommentObject();
- } catch (Exception $ex) {
- // Continue; no comments for these transactions.
- }
+ $comment_template = $this->getApplicationTransactionCommentObject();
if ($comment_template) {
$comments = $comment_template->loadAllWhere(
'transactionPHID = %s',
$this->getPHID());
foreach ($comments as $comment) {
$engine->destroyObject($comment);
}
}
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/transactions/storage/PhabricatorEditEngineConfigurationTransaction.php b/src/applications/transactions/storage/PhabricatorEditEngineConfigurationTransaction.php
index a1c44cc00..8cf7fe5b4 100644
--- a/src/applications/transactions/storage/PhabricatorEditEngineConfigurationTransaction.php
+++ b/src/applications/transactions/storage/PhabricatorEditEngineConfigurationTransaction.php
@@ -1,157 +1,153 @@
<?php
final class PhabricatorEditEngineConfigurationTransaction
extends PhabricatorApplicationTransaction {
const TYPE_NAME = 'editengine.config.name';
const TYPE_PREAMBLE = 'editengine.config.preamble';
const TYPE_ORDER = 'editengine.config.order';
const TYPE_DEFAULT = 'editengine.config.default';
const TYPE_LOCKS = 'editengine.config.locks';
const TYPE_DEFAULTCREATE = 'editengine.config.default.create';
const TYPE_ISEDIT = 'editengine.config.isedit';
const TYPE_DISABLE = 'editengine.config.disable';
const TYPE_CREATEORDER = 'editengine.order.create';
const TYPE_EDITORDER = 'editengine.order.edit';
const TYPE_SUBTYPE = 'editengine.config.subtype';
public function getApplicationName() {
return 'search';
}
public function getApplicationTransactionType() {
return PhabricatorEditEngineConfigurationPHIDType::TYPECONST;
}
- public function getApplicationTransactionCommentObject() {
- return null;
- }
-
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
$type = $this->getTransactionType();
switch ($type) {
case PhabricatorTransactions::TYPE_CREATE:
return pht(
'%s created this form configuration.',
$this->renderHandleLink($author_phid));
case self::TYPE_NAME:
if (strlen($old)) {
return pht(
'%s renamed this form from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$old,
$new);
} else {
return pht(
'%s named this form "%s".',
$this->renderHandleLink($author_phid),
$new);
}
case self::TYPE_PREAMBLE:
return pht(
'%s updated the preamble for this form.',
$this->renderHandleLink($author_phid));
case self::TYPE_ORDER:
return pht(
'%s reordered the fields in this form.',
$this->renderHandleLink($author_phid));
case self::TYPE_DEFAULT:
$key = $this->getMetadataValue('field.key');
return pht(
'%s changed the default value for field "%s".',
$this->renderHandleLink($author_phid),
$key);
case self::TYPE_LOCKS:
return pht(
'%s changed locked and hidden fields.',
$this->renderHandleLink($author_phid));
case self::TYPE_DEFAULTCREATE:
if ($new) {
return pht(
'%s added this form to the "Create" menu.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s removed this form from the "Create" menu.',
$this->renderHandleLink($author_phid));
}
case self::TYPE_ISEDIT:
if ($new) {
return pht(
'%s marked this form as an edit form.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s unmarked this form as an edit form.',
$this->renderHandleLink($author_phid));
}
case self::TYPE_DISABLE:
if ($new) {
return pht(
'%s disabled this form.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s enabled this form.',
$this->renderHandleLink($author_phid));
}
case self::TYPE_SUBTYPE:
return pht(
'%s changed the subtype of this form from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$old,
$new);
}
return parent::getTitle();
}
public function getColor() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
$type = $this->getTransactionType();
switch ($type) {
case PhabricatorTransactions::TYPE_CREATE:
return 'green';
case self::TYPE_DISABLE:
if ($new) {
return 'indigo';
} else {
return 'green';
}
}
return parent::getColor();
}
public function getIcon() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
$type = $this->getTransactionType();
switch ($type) {
case PhabricatorTransactions::TYPE_CREATE:
return 'fa-plus';
case self::TYPE_DISABLE:
if ($new) {
return 'fa-ban';
} else {
return 'fa-check';
}
}
return parent::getIcon();
}
}
diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php
index f6a27d4bc..115c7b950 100644
--- a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php
+++ b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php
@@ -1,617 +1,618 @@
<?php
final class PhabricatorApplicationTransactionCommentView
extends AphrontView {
private $submitButtonName;
private $action;
private $previewPanelID;
private $previewTimelineID;
private $previewToggleID;
private $formID;
private $statusID;
private $commentID;
private $draft;
private $requestURI;
private $showPreview = true;
private $objectPHID;
private $headerText;
private $noPermission;
private $fullWidth;
private $infoView;
private $editEngineLock;
private $noBorder;
private $requiresMFA;
private $currentVersion;
private $versionedDraft;
private $commentActions;
private $commentActionGroups = array();
private $transactionTimeline;
public function setObjectPHID($object_phid) {
$this->objectPHID = $object_phid;
return $this;
}
public function getObjectPHID() {
return $this->objectPHID;
}
public function setShowPreview($show_preview) {
$this->showPreview = $show_preview;
return $this;
}
public function getShowPreview() {
return $this->showPreview;
}
public function setRequestURI(PhutilURI $request_uri) {
$this->requestURI = $request_uri;
return $this;
}
public function getRequestURI() {
return $this->requestURI;
}
public function setCurrentVersion($current_version) {
$this->currentVersion = $current_version;
return $this;
}
public function getCurrentVersion() {
return $this->currentVersion;
}
public function setVersionedDraft(
PhabricatorVersionedDraft $versioned_draft) {
$this->versionedDraft = $versioned_draft;
return $this;
}
public function getVersionedDraft() {
return $this->versionedDraft;
}
public function setDraft(PhabricatorDraft $draft) {
$this->draft = $draft;
return $this;
}
public function getDraft() {
return $this->draft;
}
public function setSubmitButtonName($submit_button_name) {
$this->submitButtonName = $submit_button_name;
return $this;
}
public function getSubmitButtonName() {
return $this->submitButtonName;
}
public function setAction($action) {
$this->action = $action;
return $this;
}
public function getAction() {
return $this->action;
}
public function setHeaderText($text) {
$this->headerText = $text;
return $this;
}
public function setFullWidth($fw) {
$this->fullWidth = $fw;
return $this;
}
public function setInfoView(PHUIInfoView $info_view) {
$this->infoView = $info_view;
return $this;
}
public function getInfoView() {
return $this->infoView;
}
public function setCommentActions(array $comment_actions) {
assert_instances_of($comment_actions, 'PhabricatorEditEngineCommentAction');
$this->commentActions = $comment_actions;
return $this;
}
public function getCommentActions() {
return $this->commentActions;
}
public function setCommentActionGroups(array $groups) {
assert_instances_of($groups, 'PhabricatorEditEngineCommentActionGroup');
$this->commentActionGroups = $groups;
return $this;
}
public function getCommentActionGroups() {
return $this->commentActionGroups;
}
public function setNoPermission($no_permission) {
$this->noPermission = $no_permission;
return $this;
}
public function getNoPermission() {
return $this->noPermission;
}
public function setEditEngineLock(PhabricatorEditEngineLock $lock) {
$this->editEngineLock = $lock;
return $this;
}
public function getEditEngineLock() {
return $this->editEngineLock;
}
public function setRequiresMFA($requires_mfa) {
$this->requiresMFA = $requires_mfa;
return $this;
}
public function getRequiresMFA() {
return $this->requiresMFA;
}
public function setTransactionTimeline(
PhabricatorApplicationTransactionView $timeline) {
$timeline->setQuoteTargetID($this->getCommentID());
if ($this->getNoPermission() || $this->getEditEngineLock()) {
$timeline->setShouldTerminate(true);
}
$this->transactionTimeline = $timeline;
return $this;
}
public function render() {
if ($this->getNoPermission()) {
return null;
}
$lock = $this->getEditEngineLock();
if ($lock) {
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(
array(
$lock->getLockedObjectDisplayText(),
));
}
$viewer = $this->getViewer();
if (!$viewer->isLoggedIn()) {
$uri = id(new PhutilURI('/login/'))
- ->setQueryParam('next', (string)$this->getRequestURI());
+ ->replaceQueryParam('next', (string)$this->getRequestURI());
return id(new PHUIObjectBoxView())
->setFlush(true)
->appendChild(
javelin_tag(
'a',
array(
'class' => 'login-to-comment button',
'href' => $uri,
),
pht('Log In to Comment')));
}
if ($this->getRequiresMFA()) {
if (!$viewer->getIsEnrolledInMultiFactor()) {
$viewer->updateMultiFactorEnrollment();
if (!$viewer->getIsEnrolledInMultiFactor()) {
$messages = array();
$messages[] = pht(
'You must provide multi-factor credentials to comment or make '.
'changes, but you do not have multi-factor authentication '.
'configured on your account.');
$messages[] = pht(
'To continue, configure multi-factor authentication in Settings.');
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_MFA)
->setErrors($messages);
}
}
}
$data = array();
$comment = $this->renderCommentPanel();
if ($this->getShowPreview()) {
$preview = $this->renderPreviewPanel();
} else {
$preview = null;
}
if (!$this->getCommentActions()) {
Javelin::initBehavior(
'phabricator-transaction-comment-form',
array(
'formID' => $this->getFormID(),
'timelineID' => $this->getPreviewTimelineID(),
'panelID' => $this->getPreviewPanelID(),
'showPreview' => $this->getShowPreview(),
'actionURI' => $this->getAction(),
));
}
require_celerity_resource('phui-comment-form-css');
$image_uri = $viewer->getProfileImageURI();
- $image = phutil_tag(
+ $image = javelin_tag(
'div',
array(
'style' => 'background-image: url('.$image_uri.')',
- 'class' => 'phui-comment-image visual-only',
+ 'class' => 'phui-comment-image',
+ 'aural' => false,
));
$wedge = phutil_tag(
'div',
array(
'class' => 'phui-timeline-wedge',
),
'');
$badge_view = $this->renderBadgeView();
$comment_box = id(new PHUIObjectBoxView())
->setFlush(true)
->addClass('phui-comment-form-view')
->addSigil('phui-comment-form')
->appendChild(
phutil_tag(
'h3',
array(
'class' => 'aural-only',
),
pht('Add Comment')))
->appendChild($image)
->appendChild($badge_view)
->appendChild($wedge)
->appendChild($comment);
return array($comment_box, $preview);
}
private function renderCommentPanel() {
$draft_comment = '';
$draft_key = null;
if ($this->getDraft()) {
$draft_comment = $this->getDraft()->getDraft();
$draft_key = $this->getDraft()->getDraftKey();
}
$versioned_draft = $this->getVersionedDraft();
if ($versioned_draft) {
$draft_comment = $versioned_draft->getProperty('comment', '');
}
if (!$this->getObjectPHID()) {
throw new PhutilInvalidStateException('setObjectPHID', 'render');
}
$version_key = PhabricatorVersionedDraft::KEY_VERSION;
$version_value = $this->getCurrentVersion();
$form = id(new AphrontFormView())
->setUser($this->getUser())
->addSigil('transaction-append')
->setWorkflow(true)
->setFullWidth($this->fullWidth)
->setMetadata(
array(
'objectPHID' => $this->getObjectPHID(),
))
->setAction($this->getAction())
->setID($this->getFormID())
->addHiddenInput('__draft__', $draft_key)
->addHiddenInput($version_key, $version_value);
$comment_actions = $this->getCommentActions();
if ($comment_actions) {
$action_map = array();
$type_map = array();
$comment_actions = mpull($comment_actions, null, 'getKey');
$draft_actions = array();
$draft_keys = array();
if ($versioned_draft) {
$draft_actions = $versioned_draft->getProperty('actions', array());
if (!is_array($draft_actions)) {
$draft_actions = array();
}
foreach ($draft_actions as $action) {
$type = idx($action, 'type');
$comment_action = idx($comment_actions, $type);
if (!$comment_action) {
continue;
}
$value = idx($action, 'value');
$comment_action->setValue($value);
$draft_keys[] = $type;
}
}
foreach ($comment_actions as $key => $comment_action) {
$key = $comment_action->getKey();
$label = $comment_action->getLabel();
$action_map[$key] = array(
'key' => $key,
'label' => $label,
'type' => $comment_action->getPHUIXControlType(),
'spec' => $comment_action->getPHUIXControlSpecification(),
'initialValue' => $comment_action->getInitialValue(),
'groupKey' => $comment_action->getGroupKey(),
'conflictKey' => $comment_action->getConflictKey(),
'auralLabel' => pht('Remove Action: %s', $label),
'buttonText' => $comment_action->getSubmitButtonText(),
);
$type_map[$key] = $comment_action;
}
$options = $this->newCommentActionOptions($action_map);
$action_id = celerity_generate_unique_node_id();
$input_id = celerity_generate_unique_node_id();
$place_id = celerity_generate_unique_node_id();
$form->appendChild(
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'editengine.actions',
'id' => $input_id,
)));
$invisi_bar = phutil_tag(
'div',
array(
'id' => $place_id,
'class' => 'phui-comment-control-stack',
));
$action_select = id(new AphrontFormSelectControl())
->addClass('phui-comment-fullwidth-control')
->addClass('phui-comment-action-control')
->setID($action_id)
->setOptions($options);
$action_bar = phutil_tag(
'div',
array(
'class' => 'phui-comment-action-bar grouped',
),
array(
$action_select,
));
$form->appendChild($action_bar);
$info_view = $this->getInfoView();
if ($info_view) {
$form->appendChild($info_view);
}
if ($this->getRequiresMFA()) {
$message = pht(
'You will be required to provide multi-factor credentials to '.
'comment or make changes.');
$form->appendChild(
id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_MFA)
->setErrors(array($message)));
}
$form->appendChild($invisi_bar);
$form->addClass('phui-comment-has-actions');
$timeline = $this->transactionTimeline;
$view_data = array();
if ($timeline) {
$view_data = $timeline->getViewData();
}
Javelin::initBehavior(
'comment-actions',
array(
'actionID' => $action_id,
'inputID' => $input_id,
'formID' => $this->getFormID(),
'placeID' => $place_id,
'panelID' => $this->getPreviewPanelID(),
'timelineID' => $this->getPreviewTimelineID(),
'actions' => $action_map,
'showPreview' => $this->getShowPreview(),
'actionURI' => $this->getAction(),
'drafts' => $draft_keys,
'defaultButtonText' => $this->getSubmitButtonName(),
'viewData' => $view_data,
));
}
$submit_button = id(new AphrontFormSubmitControl())
->addClass('phui-comment-fullwidth-control')
->addClass('phui-comment-submit-control')
->setValue($this->getSubmitButtonName());
$form
->appendChild(
id(new PhabricatorRemarkupControl())
->setID($this->getCommentID())
->addClass('phui-comment-fullwidth-control')
->addClass('phui-comment-textarea-control')
->setCanPin(true)
->setName('comment')
->setUser($this->getUser())
->setValue($draft_comment))
->appendChild(
id(new AphrontFormSubmitControl())
->addClass('phui-comment-fullwidth-control')
->addClass('phui-comment-submit-control')
->addSigil('submit-transactions')
->setValue($this->getSubmitButtonName()));
return $form;
}
private function renderPreviewPanel() {
$preview = id(new PHUITimelineView())
->setID($this->getPreviewTimelineID());
return phutil_tag(
'div',
array(
'id' => $this->getPreviewPanelID(),
'style' => 'display: none',
'class' => 'phui-comment-preview-view',
),
$preview);
}
private function getPreviewPanelID() {
if (!$this->previewPanelID) {
$this->previewPanelID = celerity_generate_unique_node_id();
}
return $this->previewPanelID;
}
private function getPreviewTimelineID() {
if (!$this->previewTimelineID) {
$this->previewTimelineID = celerity_generate_unique_node_id();
}
return $this->previewTimelineID;
}
public function setFormID($id) {
$this->formID = $id;
return $this;
}
private function getFormID() {
if (!$this->formID) {
$this->formID = celerity_generate_unique_node_id();
}
return $this->formID;
}
private function getStatusID() {
if (!$this->statusID) {
$this->statusID = celerity_generate_unique_node_id();
}
return $this->statusID;
}
private function getCommentID() {
if (!$this->commentID) {
$this->commentID = celerity_generate_unique_node_id();
}
return $this->commentID;
}
private function newCommentActionOptions(array $action_map) {
$options = array();
$options['+'] = pht('Add Action...');
// Merge options into groups.
$groups = array();
foreach ($action_map as $key => $item) {
$group_key = $item['groupKey'];
if (!isset($groups[$group_key])) {
$groups[$group_key] = array();
}
$groups[$group_key][$key] = $item;
}
$group_specs = $this->getCommentActionGroups();
$group_labels = mpull($group_specs, 'getLabel', 'getKey');
// Reorder groups to put them in the same order as the recognized
// group definitions.
$groups = array_select_keys($groups, array_keys($group_labels)) + $groups;
// Move options with no group to the end.
$default_group = idx($groups, '');
if ($default_group) {
unset($groups['']);
$groups[''] = $default_group;
}
foreach ($groups as $group_key => $group_items) {
if (strlen($group_key)) {
$group_label = idx($group_labels, $group_key, $group_key);
$options[$group_label] = ipull($group_items, 'label');
} else {
foreach ($group_items as $key => $item) {
$options[$key] = $item['label'];
}
}
}
return $options;
}
private function renderBadgeView() {
$user = $this->getUser();
$can_use_badges = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorBadgesApplication',
$user);
if (!$can_use_badges) {
return null;
}
// Pull Badges from UserCache
$badges = $user->getRecentBadgeAwards();
$badge_view = null;
if ($badges) {
$badge_list = array();
foreach ($badges as $badge) {
$badge_view = id(new PHUIBadgeMiniView())
->setIcon($badge['icon'])
->setQuality($badge['quality'])
->setHeader($badge['name'])
->setTipDirection('E')
->setHref('/badges/view/'.$badge['id'].'/');
$badge_list[] = $badge_view;
}
$flex = new PHUIBadgeBoxView();
$flex->addItems($badge_list);
$flex->setCollapsed(true);
$badge_view = phutil_tag(
'div',
array(
'class' => 'phui-timeline-badges',
),
$flex);
}
return $badge_view;
}
}
diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php
index c2b32aa19..4d738877b 100644
--- a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php
+++ b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php
@@ -1,541 +1,542 @@
<?php
/**
* @concrete-extensible
*/
class PhabricatorApplicationTransactionView extends AphrontView {
private $transactions;
private $engine;
private $showEditActions = true;
private $isPreview;
private $objectPHID;
private $shouldTerminate = false;
private $quoteTargetID;
private $quoteRef;
private $pager;
private $renderAsFeed;
private $hideCommentOptions = false;
private $viewData = array();
public function setRenderAsFeed($feed) {
$this->renderAsFeed = $feed;
return $this;
}
public function setQuoteRef($quote_ref) {
$this->quoteRef = $quote_ref;
return $this;
}
public function getQuoteRef() {
return $this->quoteRef;
}
public function setQuoteTargetID($quote_target_id) {
$this->quoteTargetID = $quote_target_id;
return $this;
}
public function getQuoteTargetID() {
return $this->quoteTargetID;
}
public function setObjectPHID($object_phid) {
$this->objectPHID = $object_phid;
return $this;
}
public function getObjectPHID() {
return $this->objectPHID;
}
public function setIsPreview($is_preview) {
$this->isPreview = $is_preview;
return $this;
}
public function getIsPreview() {
return $this->isPreview;
}
public function setShowEditActions($show_edit_actions) {
$this->showEditActions = $show_edit_actions;
return $this;
}
public function getShowEditActions() {
return $this->showEditActions;
}
public function setMarkupEngine(PhabricatorMarkupEngine $engine) {
$this->engine = $engine;
return $this;
}
public function setTransactions(array $transactions) {
assert_instances_of($transactions, 'PhabricatorApplicationTransaction');
$this->transactions = $transactions;
return $this;
}
public function getTransactions() {
return $this->transactions;
}
public function setShouldTerminate($term) {
$this->shouldTerminate = $term;
return $this;
}
public function setPager(AphrontCursorPagerView $pager) {
$this->pager = $pager;
return $this;
}
public function getPager() {
return $this->pager;
}
public function setHideCommentOptions($hide_comment_options) {
$this->hideCommentOptions = $hide_comment_options;
return $this;
}
public function getHideCommentOptions() {
return $this->hideCommentOptions;
}
public function setViewData(array $view_data) {
$this->viewData = $view_data;
return $this;
}
public function getViewData() {
return $this->viewData;
}
public function buildEvents($with_hiding = false) {
$user = $this->getUser();
$xactions = $this->transactions;
$xactions = $this->filterHiddenTransactions($xactions);
$xactions = $this->groupRelatedTransactions($xactions);
$groups = $this->groupDisplayTransactions($xactions);
// If the viewer has interacted with this object, we hide things from
// before their most recent interaction by default. This tends to make
// very long threads much more manageable, because you don't have to
// scroll through a lot of history and can focus on just new stuff.
$show_group = null;
if ($with_hiding) {
// Find the most recent comment by the viewer.
$group_keys = array_keys($groups);
$group_keys = array_reverse($group_keys);
// If we would only hide a small number of transactions, don't hide
// anything. Just don't examine the last few keys. Also, we always
// want to show the most recent pieces of activity, so don't examine
// the first few keys either.
$group_keys = array_slice($group_keys, 2, -2);
$type_comment = PhabricatorTransactions::TYPE_COMMENT;
foreach ($group_keys as $group_key) {
$group = $groups[$group_key];
foreach ($group as $xaction) {
if ($xaction->getAuthorPHID() == $user->getPHID() &&
$xaction->getTransactionType() == $type_comment) {
// This is the most recent group where the user commented.
$show_group = $group_key;
break 2;
}
}
}
}
$events = array();
$hide_by_default = ($show_group !== null);
$set_next_page_id = false;
foreach ($groups as $group_key => $group) {
if ($hide_by_default && ($show_group === $group_key)) {
$hide_by_default = false;
$set_next_page_id = true;
}
$group_event = null;
foreach ($group as $xaction) {
$event = $this->renderEvent($xaction, $group);
$event->setHideByDefault($hide_by_default);
if (!$group_event) {
$group_event = $event;
} else {
$group_event->addEventToGroup($event);
}
if ($set_next_page_id) {
$set_next_page_id = false;
$pager = $this->getPager();
if ($pager) {
$pager->setNextPageID($xaction->getID());
}
}
}
$events[] = $group_event;
}
return $events;
}
public function render() {
if (!$this->getObjectPHID()) {
throw new PhutilInvalidStateException('setObjectPHID');
}
$view = $this->buildPHUITimelineView();
if ($this->getShowEditActions()) {
Javelin::initBehavior('phabricator-transaction-list');
}
return $view->render();
}
public function buildPHUITimelineView($with_hiding = true) {
if (!$this->getObjectPHID()) {
throw new PhutilInvalidStateException('setObjectPHID');
}
$view = id(new PHUITimelineView())
->setViewer($this->getViewer())
->setShouldTerminate($this->shouldTerminate)
->setQuoteTargetID($this->getQuoteTargetID())
->setQuoteRef($this->getQuoteRef())
->setViewData($this->getViewData());
$events = $this->buildEvents($with_hiding);
foreach ($events as $event) {
$view->addEvent($event);
}
if ($this->getPager()) {
$view->setPager($this->getPager());
}
return $view;
}
public function isTimelineEmpty() {
return !count($this->buildEvents(true));
}
protected function getOrBuildEngine() {
if (!$this->engine) {
$field = PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT;
$engine = id(new PhabricatorMarkupEngine())
->setViewer($this->getViewer());
foreach ($this->transactions as $xaction) {
if (!$xaction->hasComment()) {
continue;
}
$engine->addObject($xaction->getComment(), $field);
}
$engine->process();
$this->engine = $engine;
}
return $this->engine;
}
private function buildChangeDetailsLink(
PhabricatorApplicationTransaction $xaction) {
return javelin_tag(
'a',
array(
'href' => $xaction->getChangeDetailsURI(),
'sigil' => 'workflow',
),
pht('(Show Details)'));
}
private function buildExtraInformationLink(
PhabricatorApplicationTransaction $xaction) {
$link = $xaction->renderExtraInformationLink();
if (!$link) {
return null;
}
return phutil_tag(
'span',
array(
'class' => 'phui-timeline-extra-information',
),
array(" \xC2\xB7 ", $link));
}
protected function shouldGroupTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
return false;
}
protected function renderTransactionContent(
PhabricatorApplicationTransaction $xaction) {
$field = PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT;
$engine = $this->getOrBuildEngine();
$comment = $xaction->getComment();
if ($comment) {
if ($comment->getIsRemoved()) {
return javelin_tag(
'span',
array(
'class' => 'comment-deleted',
'sigil' => 'transaction-comment',
'meta' => array('phid' => $comment->getTransactionPHID()),
),
pht(
'This comment was removed by %s.',
$xaction->getHandle($comment->getAuthorPHID())->renderLink()));
} else if ($comment->getIsDeleted()) {
return javelin_tag(
'span',
array(
'class' => 'comment-deleted',
'sigil' => 'transaction-comment',
'meta' => array('phid' => $comment->getTransactionPHID()),
),
pht('This comment has been deleted.'));
} else if ($xaction->hasComment()) {
return javelin_tag(
'span',
array(
'class' => 'transaction-comment',
'sigil' => 'transaction-comment',
'meta' => array('phid' => $comment->getTransactionPHID()),
),
$engine->getOutput($comment, $field));
} else {
// This is an empty, non-deleted comment. Usually this happens when
// rendering previews.
return null;
}
}
return null;
}
private function filterHiddenTransactions(array $xactions) {
foreach ($xactions as $key => $xaction) {
if ($xaction->shouldHide()) {
unset($xactions[$key]);
}
}
return $xactions;
}
private function groupRelatedTransactions(array $xactions) {
$last = null;
$last_key = null;
$groups = array();
foreach ($xactions as $key => $xaction) {
if ($last && $this->shouldGroupTransactions($last, $xaction)) {
$groups[$last_key][] = $xaction;
unset($xactions[$key]);
} else {
$last = $xaction;
$last_key = $key;
}
}
foreach ($xactions as $key => $xaction) {
$xaction->attachTransactionGroup(idx($groups, $key, array()));
}
return $xactions;
}
private function groupDisplayTransactions(array $xactions) {
$groups = array();
$group = array();
foreach ($xactions as $xaction) {
if ($xaction->shouldDisplayGroupWith($group)) {
$group[] = $xaction;
} else {
if ($group) {
$groups[] = $group;
}
$group = array($xaction);
}
}
if ($group) {
$groups[] = $group;
}
foreach ($groups as $key => $group) {
$results = array();
// Sort transactions within the group by action strength, then by
// chronological order. This makes sure that multiple actions of the
// same type (like a close, then a reopen) render in the order they
// were performed.
$strength_groups = mgroup($group, 'getActionStrength');
krsort($strength_groups);
foreach ($strength_groups as $strength_group) {
foreach (msort($strength_group, 'getID') as $xaction) {
$results[] = $xaction;
}
}
$groups[$key] = $results;
}
return $groups;
}
private function renderEvent(
PhabricatorApplicationTransaction $xaction,
array $group) {
$viewer = $this->getViewer();
$event = id(new PHUITimelineEventView())
->setViewer($viewer)
->setAuthorPHID($xaction->getAuthorPHID())
->setTransactionPHID($xaction->getPHID())
->setUserHandle($xaction->getHandle($xaction->getAuthorPHID()))
->setIcon($xaction->getIcon())
->setColor($xaction->getColor())
->setHideCommentOptions($this->getHideCommentOptions())
->setIsSilent($xaction->getIsSilentTransaction())
- ->setIsMFA($xaction->getIsMFATransaction());
+ ->setIsMFA($xaction->getIsMFATransaction())
+ ->setIsLockOverride($xaction->getIsLockOverrideTransaction());
list($token, $token_removed) = $xaction->getToken();
if ($token) {
$event->setToken($token, $token_removed);
}
if (!$this->shouldSuppressTitle($xaction, $group)) {
if ($this->renderAsFeed) {
$title = $xaction->getTitleForFeed();
} else {
$title = $xaction->getTitle();
}
if ($xaction->hasChangeDetails()) {
if (!$this->isPreview) {
$details = $this->buildChangeDetailsLink($xaction);
$title = array(
$title,
' ',
$details,
);
}
}
if (!$this->isPreview) {
$more = $this->buildExtraInformationLink($xaction);
if ($more) {
$title = array($title, ' ', $more);
}
}
$event->setTitle($title);
}
if ($this->isPreview) {
$event->setIsPreview(true);
} else {
$event
->setDateCreated($xaction->getDateCreated())
->setContentSource($xaction->getContentSource())
->setAnchor($xaction->getID());
}
$transaction_type = $xaction->getTransactionType();
$comment_type = PhabricatorTransactions::TYPE_COMMENT;
$is_normal_comment = ($transaction_type == $comment_type);
if ($this->getShowEditActions() &&
!$this->isPreview &&
$is_normal_comment) {
$has_deleted_comment =
$xaction->getComment() &&
$xaction->getComment()->getIsDeleted();
$has_removed_comment =
$xaction->getComment() &&
$xaction->getComment()->getIsRemoved();
if ($xaction->getCommentVersion() > 1 && !$has_removed_comment) {
$event->setIsEdited(true);
}
if (!$has_removed_comment) {
$event->setIsNormalComment(true);
}
// If we have a place for quoted text to go and this is a quotable
// comment, pass the quote target ID to the event view.
if ($this->getQuoteTargetID()) {
if ($xaction->hasComment()) {
if (!$has_removed_comment && !$has_deleted_comment) {
$event->setQuoteTargetID($this->getQuoteTargetID());
$event->setQuoteRef($this->getQuoteRef());
}
}
}
$can_edit = PhabricatorPolicyCapability::CAN_EDIT;
if ($xaction->hasComment() || $has_deleted_comment) {
$has_edit_capability = PhabricatorPolicyFilter::hasCapability(
$viewer,
$xaction,
$can_edit);
if ($has_edit_capability && !$has_removed_comment) {
$event->setIsEditable(true);
}
if ($has_edit_capability || $viewer->getIsAdmin()) {
if (!$has_removed_comment) {
$event->setIsRemovable(true);
}
}
}
}
$comment = $this->renderTransactionContent($xaction);
if ($comment) {
$event->appendChild($comment);
}
return $event;
}
private function shouldSuppressTitle(
PhabricatorApplicationTransaction $xaction,
array $group) {
// This is a little hard-coded, but we don't have any other reasonable
// cases for now. Suppress "commented on" if there are other actions in
// the display group.
if (count($group) > 1) {
$type_comment = PhabricatorTransactions::TYPE_COMMENT;
if ($xaction->getTransactionType() == $type_comment) {
return true;
}
}
return false;
}
}
diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php
index 9e905f8cc..2d55c5f66 100644
--- a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php
+++ b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php
@@ -1,434 +1,465 @@
<?php
final class PhabricatorTypeaheadModularDatasourceController
extends PhabricatorTypeaheadDatasourceController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$request = $this->getRequest();
$viewer = $request->getUser();
$query = $request->getStr('q');
$offset = $request->getInt('offset');
$select_phid = null;
$is_browse = ($request->getURIData('action') == 'browse');
$select = $request->getStr('select');
if ($select) {
$select = phutil_json_decode($select);
$query = idx($select, 'q');
$offset = idx($select, 'offset');
$select_phid = idx($select, 'phid');
}
// Default this to the query string to make debugging a little bit easier.
$raw_query = nonempty($request->getStr('raw'), $query);
// This makes form submission easier in the debug view.
$class = nonempty($request->getURIData('class'), $request->getStr('class'));
$sources = id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorTypeaheadDatasource')
->execute();
if (isset($sources[$class])) {
$source = $sources[$class];
- $source->setParameters($request->getRequestData());
+
+ $parameters = array();
+
+ $raw_parameters = $request->getStr('parameters');
+ if (strlen($raw_parameters)) {
+ try {
+ $parameters = phutil_json_decode($raw_parameters);
+ } catch (PhutilJSONParserException $ex) {
+ return $this->newDialog()
+ ->setTitle(pht('Invalid Parameters'))
+ ->appendParagraph(
+ pht(
+ 'The HTTP parameter named "parameters" for this request is '.
+ 'not a valid JSON parameter. JSON is required. Exception: %s',
+ $ex->getMessage()))
+ ->addCancelButton('/');
+ }
+ }
+
+ $source->setParameters($parameters);
$source->setViewer($viewer);
// NOTE: Wrapping the source in a Composite datasource ensures we perform
// application visibility checks for the viewer, so we do not need to do
// those separately.
$composite = new PhabricatorTypeaheadRuntimeCompositeDatasource();
$composite->addDatasource($source);
$hard_limit = 1000;
$limit = 100;
$composite
->setViewer($viewer)
->setQuery($query)
->setRawQuery($raw_query)
->setLimit($limit + 1);
if ($is_browse) {
if (!$composite->isBrowsable()) {
return new Aphront404Response();
}
if (($offset + $limit) >= $hard_limit) {
// Offset-based paging is intrinsically slow; hard-cap how far we're
// willing to go with it.
return new Aphront404Response();
}
$composite
->setOffset($offset)
->setIsBrowse(true);
}
$results = $composite->loadResults();
if ($is_browse) {
// If this is a request for a specific token after the user clicks
// "Select", return the token in wire format so it can be added to
// the tokenizer.
if ($select_phid !== null) {
$map = mpull($results, null, 'getPHID');
$token = idx($map, $select_phid);
if (!$token) {
return new Aphront404Response();
}
$payload = array(
'key' => $token->getPHID(),
'token' => $token->getWireFormat(),
);
return id(new AphrontAjaxResponse())->setContent($payload);
}
$format = $request->getStr('format');
switch ($format) {
case 'html':
case 'dialog':
// These are the acceptable response formats.
break;
default:
// Return a dialog if format information is missing or invalid.
$format = 'dialog';
break;
}
$next_link = null;
if (count($results) > $limit) {
$results = array_slice($results, 0, $limit, $preserve_keys = true);
if (($offset + (2 * $limit)) < $hard_limit) {
$next_uri = id(new PhutilURI($request->getRequestURI()))
- ->setQueryParam('offset', $offset + $limit)
- ->setQueryParam('q', $query)
- ->setQueryParam('raw', $raw_query)
- ->setQueryParam('format', 'html');
+ ->replaceQueryParam('offset', $offset + $limit)
+ ->replaceQueryParam('format', 'html');
+
+ if ($query !== null) {
+ $next_uri->replaceQueryParam('q', $query);
+ } else {
+ $next_uri->removeQueryParam('q');
+ }
+
+ if ($raw_query !== null) {
+ $next_uri->replaceQueryParam('raw', $raw_query);
+ } else {
+ $next_uri->removeQueryParam('raw');
+ }
$next_link = javelin_tag(
'a',
array(
'href' => $next_uri,
'class' => 'typeahead-browse-more',
'sigil' => 'typeahead-browse-more',
'mustcapture' => true,
),
pht('More Results'));
} else {
// If the user has paged through more than 1K results, don't
// offer to page any further.
$next_link = javelin_tag(
'div',
array(
'class' => 'typeahead-browse-hard-limit',
),
pht('You reach the edge of the abyss.'));
}
}
$exclude = $request->getStrList('exclude');
$exclude = array_fuse($exclude);
$select = array(
'offset' => $offset,
'q' => $query,
);
$items = array();
foreach ($results as $result) {
// Disable already-selected tokens.
$disabled = isset($exclude[$result->getPHID()]);
$value = $select + array('phid' => $result->getPHID());
$value = json_encode($value);
$button = phutil_tag(
'button',
array(
'class' => 'small grey',
'name' => 'select',
'value' => $value,
'disabled' => $disabled ? 'disabled' : null,
),
pht('Select'));
$information = $this->renderBrowseResult($result, $button);
$items[] = phutil_tag(
'div',
array(
'class' => 'typeahead-browse-item grouped',
),
$information);
}
$markup = array(
$items,
$next_link,
);
if ($format == 'html') {
$content = array(
'markup' => hsprintf('%s', $markup),
);
return id(new AphrontAjaxResponse())->setContent($content);
}
$this->requireResource('typeahead-browse-css');
$this->initBehavior('typeahead-browse');
$input_id = celerity_generate_unique_node_id();
$frame_id = celerity_generate_unique_node_id();
$config = array(
'inputID' => $input_id,
'frameID' => $frame_id,
'uri' => (string)$request->getRequestURI(),
);
$this->initBehavior('typeahead-search', $config);
$search = javelin_tag(
'input',
array(
'type' => 'text',
'id' => $input_id,
'class' => 'typeahead-browse-input',
'autocomplete' => 'off',
'placeholder' => $source->getPlaceholderText(),
));
$frame = phutil_tag(
'div',
array(
'class' => 'typeahead-browse-frame',
'id' => $frame_id,
),
$markup);
$browser = array(
phutil_tag(
'div',
array(
'class' => 'typeahead-browse-header',
),
$search),
$frame,
);
$function_help = null;
if ($source->getAllDatasourceFunctions()) {
$reference_uri = '/typeahead/help/'.get_class($source).'/';
$parameters = $source->getParameters();
if ($parameters) {
$reference_uri = (string)id(new PhutilURI($reference_uri))
- ->setQueryParam('parameters', phutil_json_encode($parameters));
+ ->replaceQueryParam(
+ 'parameters',
+ phutil_json_encode($parameters));
}
$reference_link = phutil_tag(
'a',
array(
'href' => $reference_uri,
'target' => '_blank',
),
pht('Reference: Advanced Functions'));
$function_help = array(
id(new PHUIIconView())
->setIcon('fa-book'),
' ',
$reference_link,
);
}
return $this->newDialog()
->setWidth(AphrontDialogView::WIDTH_FORM)
->setRenderDialogAsDiv(true)
->setTitle($source->getBrowseTitle())
->appendChild($browser)
->setResizeX(true)
->setResizeY($frame_id)
->addFooter($function_help)
->addCancelButton('/', pht('Close'));
}
} else if ($is_browse) {
return new Aphront404Response();
} else {
$results = array();
}
$content = mpull($results, 'getWireFormat');
$content = array_values($content);
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())->setContent($content);
}
// If there's a non-Ajax request to this endpoint, show results in a tabular
// format to make it easier to debug typeahead output.
foreach ($sources as $key => $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)) {
unset($sources[$key]);
}
}
$options = array_fuse(array_keys($sources));
asort($options);
$form = id(new AphrontFormView())
->setUser($viewer)
->setAction('/typeahead/class/')
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Source Class'))
->setName('class')
->setValue($class)
->setOptions($options))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Query'))
->setName('q')
->setValue($request->getStr('q')))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Raw Query'))
->setName('raw')
->setValue($request->getStr('raw')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Query')));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Token Query'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setForm($form);
// Make "\n" delimiters more visible.
foreach ($content as $key => $row) {
$content[$key][0] = str_replace("\n", '<\n>', $row[0]);
}
$table = new AphrontTableView($content);
$table->setHeaders(
array(
pht('Name'),
pht('URI'),
pht('PHID'),
pht('Priority'),
pht('Display Name'),
pht('Display Type'),
pht('Image URI'),
pht('Priority Type'),
pht('Icon'),
pht('Closed'),
pht('Sprite'),
pht('Color'),
pht('Type'),
pht('Unique'),
pht('Auto'),
pht('Phase'),
));
$result_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Token Results (%s)', $class))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($table);
$title = pht('Typeahead Results');
$header = id(new PHUIHeaderView())
->setHeader($title);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(
$form_box,
$result_box,
));
return $this->newPage()
->setTitle($title)
->appendChild($view);
}
private function renderBrowseResult(
PhabricatorTypeaheadResult $result,
$button) {
$class = array();
$style = array();
$separator = " \xC2\xB7 ";
$class[] = 'phabricator-main-search-typeahead-result';
$name = phutil_tag(
'div',
array(
'class' => 'result-name',
),
$result->getDisplayName());
$icon = $result->getIcon();
$icon = id(new PHUIIconView())->setIcon($icon);
$attributes = $result->getAttributes();
$attributes = phutil_implode_html($separator, $attributes);
$attributes = array($icon, ' ', $attributes);
$closed = $result->getClosed();
if ($closed) {
$class[] = 'result-closed';
$attributes = array($closed, $separator, $attributes);
}
$attributes = phutil_tag(
'div',
array(
'class' => 'result-type',
),
$attributes);
$image = $result->getImageURI();
if ($image) {
$style[] = 'background-image: url('.$image.');';
$class[] = 'has-image';
}
return phutil_tag(
'div',
array(
'class' => implode(' ', $class),
'style' => implode(' ', $style),
),
array(
$button,
$name,
$attributes,
));
}
}
diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php
index 2e369a3f6..e077d7a7e 100644
--- a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php
+++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php
@@ -1,595 +1,607 @@
<?php
/**
* @task functions Token Functions
*/
abstract class PhabricatorTypeaheadDatasource extends Phobject {
private $viewer;
private $query;
private $rawQuery;
private $offset;
private $limit;
private $parameters = array();
private $functionStack = array();
private $isBrowse;
private $phase = self::PHASE_CONTENT;
const PHASE_PREFIX = 'prefix';
const PHASE_CONTENT = 'content';
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
public function getLimit() {
return $this->limit;
}
public function setOffset($offset) {
$this->offset = $offset;
return $this;
}
public function getOffset() {
return $this->offset;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setRawQuery($raw_query) {
$this->rawQuery = $raw_query;
return $this;
}
public function getPrefixQuery() {
return phutil_utf8_strtolower($this->getRawQuery());
}
public function getRawQuery() {
return $this->rawQuery;
}
public function setQuery($query) {
$this->query = $query;
return $this;
}
public function getQuery() {
return $this->query;
}
public function setParameters(array $params) {
$this->parameters = $params;
return $this;
}
public function getParameters() {
return $this->parameters;
}
public function getParameter($name, $default = null) {
return idx($this->parameters, $name, $default);
}
public function setIsBrowse($is_browse) {
$this->isBrowse = $is_browse;
return $this;
}
public function getIsBrowse() {
return $this->isBrowse;
}
public function setPhase($phase) {
$this->phase = $phase;
return $this;
}
public function getPhase() {
return $this->phase;
}
public function getDatasourceURI() {
- $uri = new PhutilURI('/typeahead/class/'.get_class($this).'/');
- $uri->setQueryParams($this->parameters);
- return (string)$uri;
+ $params = $this->newURIParameters();
+ $uri = new PhutilURI('/typeahead/class/'.get_class($this).'/', $params);
+ return phutil_string_cast($uri);
}
public function getBrowseURI() {
if (!$this->isBrowsable()) {
return null;
}
- $uri = new PhutilURI('/typeahead/browse/'.get_class($this).'/');
- $uri->setQueryParams($this->parameters);
- return (string)$uri;
+ $params = $this->newURIParameters();
+ $uri = new PhutilURI('/typeahead/browse/'.get_class($this).'/', $params);
+ return phutil_string_cast($uri);
+ }
+
+ private function newURIParameters() {
+ if (!$this->parameters) {
+ return array();
+ }
+
+ $map = array(
+ 'parameters' => phutil_json_encode($this->parameters),
+ );
+
+ return $map;
}
abstract public function getPlaceholderText();
public function getBrowseTitle() {
return get_class($this);
}
abstract public function getDatasourceApplicationClass();
abstract public function loadResults();
protected function loadResultsForPhase($phase, $limit) {
// By default, sources just load all of their results in every phase and
// rely on filtering at a higher level to sequence phases correctly.
$this->setLimit($limit);
return $this->loadResults();
}
protected function didLoadResults(array $results) {
return $results;
}
public static function tokenizeString($string) {
$string = phutil_utf8_strtolower($string);
$string = trim($string);
if (!strlen($string)) {
return array();
}
// NOTE: Splitting on "(" and ")" is important for milestones.
$tokens = preg_split('/[\s\[\]\(\)-]+/u', $string);
$tokens = array_unique($tokens);
// Make sure we don't return the empty token, as this will boil down to a
// JOIN against every token.
foreach ($tokens as $key => $value) {
if (!strlen($value)) {
unset($tokens[$key]);
}
}
return array_values($tokens);
}
public function getTokens() {
return self::tokenizeString($this->getRawQuery());
}
protected function executeQuery(
PhabricatorCursorPagedPolicyAwareQuery $query) {
return $query
->setViewer($this->getViewer())
->setOffset($this->getOffset())
->setLimit($this->getLimit())
->execute();
}
/**
* Can the user browse through results from this datasource?
*
* Browsable datasources allow the user to switch from typeahead mode to
* a browse mode where they can scroll through all results.
*
* By default, datasources are browsable, but some datasources can not
* generate a meaningful result set or can't filter results on the server.
*
* @return bool
*/
public function isBrowsable() {
return true;
}
/**
* Filter a list of results, removing items which don't match the query
* tokens.
*
* This is useful for datasources which return a static list of hard-coded
* or configured results and can't easily do query filtering in a real
* query class. Instead, they can just build the entire result set and use
* this method to filter it.
*
* For datasources backed by database objects, this is often much less
* efficient than filtering at the query level.
*
* @param list<PhabricatorTypeaheadResult> List of typeahead results.
* @return list<PhabricatorTypeaheadResult> Filtered results.
*/
protected function filterResultsAgainstTokens(array $results) {
$tokens = $this->getTokens();
if (!$tokens) {
return $results;
}
$map = array();
foreach ($tokens as $token) {
$map[$token] = strlen($token);
}
foreach ($results as $key => $result) {
$rtokens = self::tokenizeString($result->getName());
// For each token in the query, we need to find a match somewhere
// in the result name.
foreach ($map as $token => $length) {
// Look for a match.
$match = false;
foreach ($rtokens as $rtoken) {
if (!strncmp($rtoken, $token, $length)) {
// This part of the result name has the query token as a prefix.
$match = true;
break;
}
}
if (!$match) {
// We didn't find a match for this query token, so throw the result
// away. Try with the next result.
unset($results[$key]);
break;
}
}
}
return $results;
}
protected function newFunctionResult() {
return id(new PhabricatorTypeaheadResult())
->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION)
->setIcon('fa-asterisk')
->addAttribute(pht('Function'));
}
public function newInvalidToken($name) {
return id(new PhabricatorTypeaheadTokenView())
->setValue($name)
->setIcon('fa-exclamation-circle')
->setTokenType(PhabricatorTypeaheadTokenView::TYPE_INVALID);
}
public function renderTokens(array $values) {
$phids = array();
$setup = array();
$tokens = array();
foreach ($values as $key => $value) {
if (!self::isFunctionToken($value)) {
$phids[$key] = $value;
} else {
$function = $this->parseFunction($value);
if ($function) {
$setup[$function['name']][$key] = $function;
} else {
$name = pht('Invalid Function: %s', $value);
$tokens[$key] = $this->newInvalidToken($name)
->setKey($value);
}
}
}
// Give special non-function tokens which are also not PHIDs (like statuses
// and priorities) an opportunity to render.
$type_unknown = PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN;
$special = array();
foreach ($values as $key => $value) {
if (phid_get_type($value) == $type_unknown) {
$special[$key] = $value;
}
}
if ($special) {
$special_tokens = $this->renderSpecialTokens($special);
foreach ($special_tokens as $key => $token) {
$tokens[$key] = $token;
unset($phids[$key]);
}
}
if ($phids) {
$handles = $this->getViewer()->loadHandles($phids);
foreach ($phids as $key => $phid) {
$handle = $handles[$phid];
$tokens[$key] = PhabricatorTypeaheadTokenView::newFromHandle($handle);
}
}
if ($setup) {
foreach ($setup as $function_name => $argv_list) {
// Render the function tokens.
$function_tokens = $this->renderFunctionTokens(
$function_name,
ipull($argv_list, 'argv'));
// Rekey the function tokens using the original array keys.
$function_tokens = array_combine(
array_keys($argv_list),
$function_tokens);
// For any functions which were invalid, set their value to the
// original input value before it was parsed.
foreach ($function_tokens as $key => $token) {
$type = $token->getTokenType();
if ($type == PhabricatorTypeaheadTokenView::TYPE_INVALID) {
$token->setKey($values[$key]);
}
}
$tokens += $function_tokens;
}
}
return array_select_keys($tokens, array_keys($values));
}
protected function renderSpecialTokens(array $values) {
return array();
}
/* -( Token Functions )---------------------------------------------------- */
/**
* @task functions
*/
public function getDatasourceFunctions() {
return array();
}
/**
* @task functions
*/
public function getAllDatasourceFunctions() {
return $this->getDatasourceFunctions();
}
/**
* @task functions
*/
protected function canEvaluateFunction($function) {
return $this->shouldStripFunction($function);
}
/**
* @task functions
*/
protected function shouldStripFunction($function) {
$functions = $this->getDatasourceFunctions();
return isset($functions[$function]);
}
/**
* @task functions
*/
protected function evaluateFunction($function, array $argv_list) {
throw new PhutilMethodNotImplementedException();
}
/**
* @task functions
*/
protected function evaluateValues(array $values) {
return $values;
}
/**
* @task functions
*/
public function evaluateTokens(array $tokens) {
$results = array();
$evaluate = array();
foreach ($tokens as $token) {
if (!self::isFunctionToken($token)) {
$results[] = $token;
} else {
// Put a placeholder in the result list so that we retain token order
// when possible. We'll overwrite this below.
$results[] = null;
$evaluate[last_key($results)] = $token;
}
}
$results = $this->evaluateValues($results);
foreach ($evaluate as $result_key => $function) {
$function = $this->parseFunction($function);
if (!$function) {
throw new PhabricatorTypeaheadInvalidTokenException();
}
$name = $function['name'];
$argv = $function['argv'];
$evaluated_tokens = $this->evaluateFunction($name, array($argv));
if (!$evaluated_tokens) {
unset($results[$result_key]);
} else {
$is_first = true;
foreach ($evaluated_tokens as $phid) {
if ($is_first) {
$results[$result_key] = $phid;
$is_first = false;
} else {
$results[] = $phid;
}
}
}
}
$results = array_values($results);
$results = $this->didEvaluateTokens($results);
return $results;
}
/**
* @task functions
*/
protected function didEvaluateTokens(array $results) {
return $results;
}
/**
* @task functions
*/
public static function isFunctionToken($token) {
// We're looking for a "(" so that a string like "members(q" is identified
// and parsed as a function call. This allows us to start generating
// results immediately, before the user fully types out "members(quack)".
return (strpos($token, '(') !== false);
}
/**
* @task functions
*/
protected function parseFunction($token, $allow_partial = false) {
$matches = null;
if ($allow_partial) {
$ok = preg_match('/^([^(]+)\((.*?)\)?\z/', $token, $matches);
} else {
$ok = preg_match('/^([^(]+)\((.*)\)\z/', $token, $matches);
}
if (!$ok) {
if (!$allow_partial) {
throw new PhabricatorTypeaheadInvalidTokenException(
pht(
'Unable to parse function and arguments for token "%s".',
$token));
}
return null;
}
$function = trim($matches[1]);
if (!$this->canEvaluateFunction($function)) {
if (!$allow_partial) {
throw new PhabricatorTypeaheadInvalidTokenException(
pht(
'This datasource ("%s") can not evaluate the function "%s(...)".',
get_class($this),
$function));
}
return null;
}
// TODO: There is currently no way to quote characters in arguments, so
// some characters can't be argument characters. Replace this with a real
// parser once we get use cases.
$argv = $matches[2];
$argv = trim($argv);
if (!strlen($argv)) {
$argv = array();
} else {
$argv = preg_split('/,/', $matches[2]);
foreach ($argv as $key => $arg) {
$argv[$key] = trim($arg);
}
}
foreach ($argv as $key => $arg) {
if (self::isFunctionToken($arg)) {
$subfunction = $this->parseFunction($arg);
$results = $this->evaluateFunction(
$subfunction['name'],
array($subfunction['argv']));
$argv[$key] = head($results);
}
}
return array(
'name' => $function,
'argv' => $argv,
);
}
/**
* @task functions
*/
public function renderFunctionTokens($function, array $argv_list) {
throw new PhutilMethodNotImplementedException();
}
/**
* @task functions
*/
public function setFunctionStack(array $function_stack) {
$this->functionStack = $function_stack;
return $this;
}
/**
* @task functions
*/
public function getFunctionStack() {
return $this->functionStack;
}
/**
* @task functions
*/
protected function getCurrentFunction() {
return nonempty(last($this->functionStack), null);
}
protected function renderTokensFromResults(array $results, array $values) {
$tokens = array();
foreach ($values as $key => $value) {
if (empty($results[$value])) {
continue;
}
$tokens[$key] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult(
$results[$value]);
}
return $tokens;
}
public function getWireTokens(array $values) {
// TODO: This is a bit hacky for now: we're sort of generating wire
// results, rendering them, then reverting them back to wire results. This
// is pretty silly. It would probably be much cleaner to make
// renderTokens() call this method instead, then render from the result
// structure.
$rendered = $this->renderTokens($values);
$tokens = array();
foreach ($rendered as $key => $render) {
$tokens[$key] = id(new PhabricatorTypeaheadResult())
->setPHID($render->getKey())
->setIcon($render->getIcon())
->setColor($render->getColor())
->setDisplayName($render->getValue())
->setTokenType($render->getTokenType());
}
return mpull($tokens, 'getWireFormat', 'getPHID');
}
}
diff --git a/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php b/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php
index 14cbe726d..b13cf351b 100644
--- a/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php
+++ b/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php
@@ -1,225 +1,236 @@
<?php
final class PhabricatorTypeaheadResult extends Phobject {
private $name;
private $uri;
private $phid;
private $priorityString;
private $displayName;
private $displayType;
private $imageURI;
private $priorityType;
private $imageSprite;
private $icon;
private $color;
private $closed;
private $tokenType;
private $unique;
private $autocomplete;
private $attributes = array();
private $phase;
+ private $availabilityColor;
public function setIcon($icon) {
$this->icon = $icon;
return $this;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function setURI($uri) {
$this->uri = $uri;
return $this;
}
public function setPHID($phid) {
$this->phid = $phid;
return $this;
}
public function setPriorityString($priority_string) {
$this->priorityString = $priority_string;
return $this;
}
public function setDisplayName($display_name) {
$this->displayName = $display_name;
return $this;
}
public function setDisplayType($display_type) {
$this->displayType = $display_type;
return $this;
}
public function setImageURI($image_uri) {
$this->imageURI = $image_uri;
return $this;
}
public function setPriorityType($priority_type) {
$this->priorityType = $priority_type;
return $this;
}
public function setImageSprite($image_sprite) {
$this->imageSprite = $image_sprite;
return $this;
}
public function setClosed($closed) {
$this->closed = $closed;
return $this;
}
public function getName() {
return $this->name;
}
public function getDisplayName() {
return coalesce($this->displayName, $this->getName());
}
public function getIcon() {
return nonempty($this->icon, $this->getDefaultIcon());
}
public function getPHID() {
return $this->phid;
}
public function setUnique($unique) {
$this->unique = $unique;
return $this;
}
public function setTokenType($type) {
$this->tokenType = $type;
return $this;
}
public function getTokenType() {
if ($this->closed && !$this->tokenType) {
return PhabricatorTypeaheadTokenView::TYPE_DISABLED;
}
return $this->tokenType;
}
public function setColor($color) {
$this->color = $color;
return $this;
}
public function getColor() {
return $this->color;
}
public function setAutocomplete($autocomplete) {
$this->autocomplete = $autocomplete;
return $this;
}
public function getAutocomplete() {
return $this->autocomplete;
}
public function getSortKey() {
// Put unique results (special parameter functions) ahead of other
// results.
if ($this->unique) {
$prefix = 'A';
} else {
$prefix = 'B';
}
return $prefix.phutil_utf8_strtolower($this->getName());
}
public function getWireFormat() {
$data = array(
$this->name,
$this->uri ? (string)$this->uri : null,
$this->phid,
$this->priorityString,
$this->displayName,
$this->displayType,
$this->imageURI ? (string)$this->imageURI : null,
$this->priorityType,
$this->getIcon(),
$this->closed,
$this->imageSprite ? (string)$this->imageSprite : null,
$this->color,
$this->tokenType,
$this->unique ? 1 : null,
$this->autocomplete,
$this->phase,
+ $this->availabilityColor,
);
while (end($data) === null) {
array_pop($data);
}
return $data;
}
/**
* If the datasource did not specify an icon explicitly, try to select a
* default based on PHID type.
*/
private function getDefaultIcon() {
static $icon_map;
if ($icon_map === null) {
$types = PhabricatorPHIDType::getAllTypes();
$map = array();
foreach ($types as $type) {
$icon = $type->getTypeIcon();
if ($icon !== null) {
$map[$type->getTypeConstant()] = $icon;
}
}
$icon_map = $map;
}
$phid_type = phid_get_type($this->phid);
if (isset($icon_map[$phid_type])) {
return $icon_map[$phid_type];
}
return null;
}
public function getImageURI() {
return $this->imageURI;
}
public function getClosed() {
return $this->closed;
}
public function resetAttributes() {
$this->attributes = array();
return $this;
}
public function getAttributes() {
return $this->attributes;
}
public function addAttribute($attribute) {
$this->attributes[] = $attribute;
return $this;
}
public function setPhase($phase) {
$this->phase = $phase;
return $this;
}
public function getPhase() {
return $this->phase;
}
+ public function setAvailabilityColor($availability_color) {
+ $this->availabilityColor = $availability_color;
+ return $this;
+ }
+
+ public function getAvailabilityColor() {
+ return $this->availabilityColor;
+ }
+
}
diff --git a/src/applications/typeahead/view/PhabricatorTypeaheadTokenView.php b/src/applications/typeahead/view/PhabricatorTypeaheadTokenView.php
index 56867d827..e0a5270e8 100644
--- a/src/applications/typeahead/view/PhabricatorTypeaheadTokenView.php
+++ b/src/applications/typeahead/view/PhabricatorTypeaheadTokenView.php
@@ -1,167 +1,204 @@
<?php
final class PhabricatorTypeaheadTokenView
extends AphrontTagView {
const TYPE_OBJECT = 'object';
const TYPE_DISABLED = 'disabled';
const TYPE_FUNCTION = 'function';
const TYPE_INVALID = 'invalid';
private $key;
private $icon;
private $color;
private $inputName;
private $value;
private $tokenType = self::TYPE_OBJECT;
+ private $availabilityColor;
public static function newFromTypeaheadResult(
PhabricatorTypeaheadResult $result) {
return id(new PhabricatorTypeaheadTokenView())
->setKey($result->getPHID())
->setIcon($result->getIcon())
->setColor($result->getColor())
->setValue($result->getDisplayName())
->setTokenType($result->getTokenType());
}
public static function newFromHandle(
PhabricatorObjectHandle $handle) {
$token = id(new PhabricatorTypeaheadTokenView())
->setKey($handle->getPHID())
->setValue($handle->getFullName())
->setIcon($handle->getTokenIcon());
if ($handle->isDisabled() ||
$handle->getStatus() == PhabricatorObjectHandle::STATUS_CLOSED) {
$token->setTokenType(self::TYPE_DISABLED);
} else {
$token->setColor($handle->getTagColor());
}
+ $availability = $handle->getAvailability();
+ $color = null;
+ switch ($availability) {
+ case PhabricatorObjectHandle::AVAILABILITY_PARTIAL:
+ $color = PHUITagView::COLOR_ORANGE;
+ break;
+ case PhabricatorObjectHandle::AVAILABILITY_NONE:
+ $color = PHUITagView::COLOR_RED;
+ break;
+ }
+
+ if ($color !== null) {
+ $token->setAvailabilityColor($color);
+ }
+
return $token;
}
public function isInvalid() {
return ($this->getTokenType() == self::TYPE_INVALID);
}
public function setKey($key) {
$this->key = $key;
return $this;
}
public function getKey() {
return $this->key;
}
public function setTokenType($token_type) {
$this->tokenType = $token_type;
return $this;
}
public function getTokenType() {
return $this->tokenType;
}
public function setInputName($input_name) {
$this->inputName = $input_name;
return $this;
}
public function getInputName() {
return $this->inputName;
}
public function setIcon($icon) {
$this->icon = $icon;
return $this;
}
public function getIcon() {
return $this->icon;
}
public function setColor($color) {
$this->color = $color;
return $this;
}
public function getColor() {
return $this->color;
}
public function setValue($value) {
$this->value = $value;
return $this;
}
public function getValue() {
return $this->value;
}
protected function getTagName() {
return 'a';
}
+ public function setAvailabilityColor($availability_color) {
+ $this->availabilityColor = $availability_color;
+ return $this;
+ }
+
+ public function getAvailabilityColor() {
+ return $this->availabilityColor;
+ }
+
protected function getTagAttributes() {
$classes = array();
$classes[] = 'jx-tokenizer-token';
switch ($this->getTokenType()) {
case self::TYPE_FUNCTION:
$classes[] = 'jx-tokenizer-token-function';
break;
case self::TYPE_INVALID:
$classes[] = 'jx-tokenizer-token-invalid';
break;
case self::TYPE_DISABLED:
$classes[] = 'jx-tokenizer-token-disabled';
break;
case self::TYPE_OBJECT:
default:
break;
}
$classes[] = $this->getColor();
return array(
'class' => $classes,
);
}
protected function getTagContent() {
$input_name = $this->getInputName();
if ($input_name) {
$input_name .= '[]';
}
$value = $this->getValue();
+ $availability = null;
+ $availability_color = $this->getAvailabilityColor();
+ if ($availability_color) {
+ $availability = phutil_tag(
+ 'span',
+ array(
+ 'class' => 'phui-tag-dot phui-tag-color-'.$availability_color,
+ ));
+ }
+
+ $icon_view = null;
$icon = $this->getIcon();
if ($icon) {
- $value = array(
- phutil_tag(
- 'span',
- array(
- 'class' => 'phui-icon-view phui-font-fa '.$icon,
- )),
- $value,
- );
+ $icon_view = phutil_tag(
+ 'span',
+ array(
+ 'class' => 'phui-icon-view phui-font-fa '.$icon,
+ ));
}
return array(
- $value,
+ array(
+ $icon_view,
+ $availability,
+ $value,
+ ),
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => $input_name,
'value' => $this->getKey(),
)),
phutil_tag('span', array('class' => 'jx-tokenizer-x-placeholder'), ''),
);
}
}
diff --git a/src/docs/user/configuration/configuration_locked.diviner b/src/docs/user/configuration/configuration_locked.diviner
index 958124c38..f96adc2d8 100644
--- a/src/docs/user/configuration/configuration_locked.diviner
+++ b/src/docs/user/configuration/configuration_locked.diviner
@@ -1,121 +1,170 @@
@title Configuration Guide: Locked and Hidden Configuration
@group config
Details about locked and hidden configuration.
Overview
========
Some configuration options are **Locked** or **Hidden**. If an option has one
of these attributes, it means:
- **Locked Configuration**: This setting can not be written from the web UI.
- **Hidden Configuration**: This setting can not be read or written from
the web UI.
This document explains these attributes in more detail.
Locked Configuration
====================
**Locked Configuration** can not be edited from the web UI. In general, you
can edit it from the CLI instead, with `bin/config`:
```
phabricator/ $ ./bin/config set <key> <value>
```
Some configuration options take complicated values which can be difficult
to escape properly for the shell. The easiest way to set these options is
to use the `--stdin` flag. First, put your desired value in a `config.json`
file:
```name=config.json, lang=json
{
"duck": "quack",
"cow": "moo"
}
```
Then, set it with `--stdin` like this:
```
phabricator/ $ ./bin/config set <key> --stdin < config.json
```
A few settings have alternate CLI tools. Refer to the setting page for
details.
Note that these settings can not be written to the database, even from the
CLI.
Locked values can not be unlocked: they are locked because of what the setting
does or how the setting operates. Some of the reasons configuration options are
locked include:
**Required for bootstrapping**: Some options, like `mysql.host`, must be
available before Phabricator can read configuration from the database.
If you stored `mysql.host` only in the database, Phabricator would not know how
to connect to the database in order to read the value in the first place.
These options must be provided in a configuration source which is read earlier
in the bootstrapping process, before Phabricator connects to the database.
**Errors could not be fixed from the web UI**: Some options, like
`phabricator.base-uri`, can effectively disable the web UI if they are
configured incorrectly.
If these options could be configured from the web UI, you could not fix them if
you made a mistake (because the web UI would no longer work, so you could not
load the page to change the value).
We require these options to be edited from the CLI to make sure the editor has
access to fix any mistakes.
**Attackers could gain greater access**: Some options could be modified by an
attacker who has gained access to an administrator account in order to gain
greater access.
For example, an attacker who could modify `cluster.mailers` (and other
similar options), could potentially reconfigure Phabricator to send mail
through an evil server they controlled, then trigger password resets on other
user accounts to compromise them.
We require these options to be edited from the CLI to make sure the editor
has full access to the install.
Hidden Configuration
====================
**Hidden Configuration** is similar to locked configuration, but also can not
be //read// from the web UI.
In almost all cases, configuration is hidden because it is some sort of secret
key or access token for an external service. These values are hidden from the
web UI to prevent administrators (or attackers who have compromised
administrator accounts) from reading them.
You can review (and edit) hidden configuration from the CLI:
```
phabricator/ $ ./bin/config get <key>
phabricator/ $ ./bin/config set <key> <value>
```
+Locked Configuration With Database Values
+=========================================
+
+You may receive a setup issue warning you that a locked configuration key has a
+value set in the database. Most commonly, this is because:
+
+ - In some earlier version of Phabricator, this configuration was not locked.
+ - In the past, you or some other administrator used the web UI to set a
+ value. This value was written to the database.
+ - In a later version of the software, the value became locked.
+
+When Phabricator was originally released, locked configuration did not yet
+exist. Locked configuration was introduced later, and then configuration options
+were gradually locked for a long time after that.
+
+In some cases the meaning of a value changed and it became possible to use it
+to break an install or the configuration became a security risk. In other
+cases, we identified an existing security risk or arrived at some other reason
+to lock the value.
+
+Locking values was more common in the past, and it is now relatively rare for
+an unlocked value to become locked: when new values are introduced, they are
+generally locked or hidden appropriately. In most cases, this setup issue only
+affects installs that have used Phabricator for a long time.
+
+At time of writing (February 2019), Phabricator currently respects these old
+database values. However, some future version of Phabricator will refuse to
+read locked configuration from the database, because this improves security if
+an attacker manages to find a way to bypass restrictions on editing locked
+configuration from the web UI.
+
+To clear this setup warning and avoid surprise behavioral changes in the future,
+you should move these configuration values from the database to a local config
+file. Usually, you'll do this by first copying the value from the database:
+
+```
+phabricator/ $ ./bin/config set <key> <value>
+```
+
+...and then removing the database value:
+
+```
+phabricator/ $ ./bin/config delete --database <key>
+```
+
+See @{Configuration User Guide: Advanced Configuration} for some more detailed
+discussion of different configuration sources.
+
+
Next Steps
==========
Continue by:
- learning more about advanced options with
@{Configuration User Guide: Advanced Configuration}; or
- returning to the @{article: Configuration Guide}.
diff --git a/src/docs/user/configuration/configuring_accounts_and_registration.diviner b/src/docs/user/configuration/configuring_accounts_and_registration.diviner
index 05d11b11f..a56d7377c 100644
--- a/src/docs/user/configuration/configuring_accounts_and_registration.diviner
+++ b/src/docs/user/configuration/configuring_accounts_and_registration.diviner
@@ -1,67 +1,83 @@
@title Configuring Accounts and Registration
@group config
Describes how to configure user access to Phabricator.
-= Overview =
+Overview
+========
Phabricator supports a number of login systems. You can enable or disable these
systems to configure who can register for and access your install, and how users
with existing accounts can login.
Methods of logging in are called **Authentication Providers**. For example,
there is a "Username/Password" authentication provider available, which allows
users to log in with a traditional username and password. Other providers
support logging in with other credentials. For example:
- **LDAP:** Users use LDAP credentials to log in or register.
- **OAuth:** Users use accounts on a supported OAuth2 provider (like
GitHub, Facebook, or Google) to log in or register.
- **Other Providers:** More providers are available, and Phabricator
can be extended with custom providers. See the "Auth" application for
a list of available providers.
By default, no providers are enabled. You must use the "Auth" application to
add one or more providers after you complete the installation process.
After you add a provider, you can link it to existing accounts (for example,
associate an existing Phabricator account with a GitHub OAuth account) or users
can use it to register new accounts (assuming you enable these options).
-= Recovering Inaccessible Accounts =
+
+Recovering Inaccessible Accounts
+================================
If you accidentally lock yourself out of Phabricator (for example, by disabling
-all authentication providers), you can use the `bin/auth`
-script to recover access to an account. To recover access, run:
+all authentication providers), you can normally use the "send a login link"
+action from the login screen to email yourself a login link and regain access
+to your account.
+
+If that isn't working (perhaps because you haven't configured email yet), you
+can use the `bin/auth` script to recover access to an account. To recover
+access, run:
- phabricator/ $ ./bin/auth recover <username>
+```
+phabricator/ $ ./bin/auth recover <username>
+```
...where `<username>` is the account username you want to recover access
to. This will generate a link which will log you in as the specified user.
-= Managing Accounts with the Web Console =
+
+Managing Accounts with the Web Console
+======================================
To manage accounts from the web, login as an administrator account and go to
`/people/` or click "People" on the homepage. Provided you're an admin,
you'll see options to create or edit accounts.
-= Manually Creating New Accounts =
+
+Manually Creating New Accounts
+==============================
There are two ways to manually create new accounts: via the web UI using
the "People" application (this is easiest), or via the CLI using the
`accountadmin` binary (this has a few more options).
To use the CLI script, run:
phabricator/ $ ./bin/accountadmin
Some options (like changing certain account flags) are only available from
the CLI. You can also use this script to make a user
an administrator (if you accidentally remove your admin flag) or to create an
administrative account.
-= Next Steps =
+
+Next Steps
+==========
Continue by:
- returning to the @{article:Configuration Guide}.
diff --git a/src/docs/user/configuration/configuring_outbound_email.diviner b/src/docs/user/configuration/configuring_outbound_email.diviner
index 4d18ba0eb..884e4e7fd 100644
--- a/src/docs/user/configuration/configuring_outbound_email.diviner
+++ b/src/docs/user/configuration/configuring_outbound_email.diviner
@@ -1,441 +1,510 @@
@title Configuring Outbound Email
@group config
Instructions for configuring Phabricator to send email and other types of
messages, like text messages.
Overview
========
Phabricator sends outbound messages through "mailers". Most mailers send
email and most messages are email messages, but mailers may also send other
types of messages (like text messages).
Phabricator can send outbound messages through multiple different mailers,
including a local mailer or various third-party services. Options include:
| Send Mail With | Setup | Cost | Inbound | Media | Notes |
|----------------|-------|------|---------|-------|-------|
| Postmark | Easy | Cheap | Yes | Email | Recommended |
| Mailgun | Easy | Cheap | Yes | Email | Recommended |
| Amazon SES | Easy | Cheap | No | Email | |
| SendGrid | Medium | Cheap | Yes | Email | |
| Twilio | Easy | Cheap | No | SMS | Recommended |
| Amazon SNS | Easy | Cheap | No | SMS | Recommended |
| External SMTP | Medium | Varies | No | Email | Gmail, etc. |
| Local SMTP | Hard | Free | No | Email | sendmail, postfix, etc |
| Custom | Hard | Free | No | All | Write a custom mailer. |
| Drop in a Hole | Easy | Free | No | All | Drops mail in a deep, dark hole. |
See below for details on how to select and configure mail delivery for each
mailer.
For email, Postmark or Mailgun are recommended because they make it easy to
set up inbound and outbound mail and have good track records in our production
services. Other services will also generally work well, but they may be more
difficult to set up.
For SMS, Twilio or SNS are recommended. They're also your only upstream
options.
If you have some internal mail or messaging service you'd like to use you can
also write a custom mailer, but this requires digging into the code.
Phabricator sends mail in the background, so the daemons need to be running for
it to be able to deliver mail. You should receive setup warnings if they are
not. For more information on using daemons, see
@{article:Managing Daemons with phd}.
-Basics
-======
+Outbound "From" and "To" Addresses
+==================================
-Before configuring outbound mail, you should first set up
-`metamta.default-address` in Configuration. This determines where mail is sent
-"From" by default.
+When Phabricator sends outbound mail, it must select some "From" address to
+send mail from, since mailers require this.
-If your domain is `example.org`, set this to something
-like `noreply@example.org`.
+When mail only has "CC" recipients, Phabricator generates a dummy "To" address,
+since some mailers require this and some users write mail rules that depend
+on whether they appear in the "To" or "CC" line.
-Ideally, this should be a valid, deliverable address that doesn't bounce if
-users accidentally send mail to it.
+In both cases, the address should ideally correspond to a valid, deliverable
+mailbox that accepts the mail and then simply discards it. If the address is
+not valid, some outbound mail will bounce, and users will receive bounces when
+they "Reply All" even if the other recipients for the message are valid. In
+contrast, if the address is a real user address, that user will receive a lot
+of mail they probably don't want.
+
+If you plan to configure //inbound// mail later, you usually don't need to do
+anything. Phabricator will automatically create a `noreply@` mailbox which
+works the right way (accepts and discards all mail it receives) and
+automatically use it when generating addresses.
+
+If you don't plan to configure inbound mail, you may need to configure an
+address for Phabricator to use. You can do this by setting
+`metamta.default-address`.
Configuring Mailers
===================
Configure one or more mailers by listing them in the the `cluster.mailers`
configuration option. Most installs only need to configure one mailer, but you
can configure multiple mailers to provide greater availability in the event of
a service disruption.
A valid `cluster.mailers` configuration looks something like this:
```lang=json
[
{
"key": "mycompany-mailgun",
"type": "mailgun",
"options": {
"domain": "mycompany.com",
"api-key": "..."
}
},
...
]
```
The supported keys for each mailer are:
- `key`: Required string. A unique name for this mailer.
- `type`: Required string. Identifies the type of mailer. See below for
options.
- `priority`: Optional string. Advanced option which controls load balancing
and failover behavior. See below for details.
- `options`: Optional map. Additional options for the mailer type.
- `inbound`: Optional bool. Use `false` to prevent this mailer from being
used to receive inbound mail.
- `outbound`: Optional bool. Use `false` to prevent this mailer from being
used to send outbound mail.
- `media`: Optional list<string>. Some mailers support delivering multiple
types of messages (like Email and SMS). If you want to configure a mailer
to support only a subset of possible message types, list only those message
types. Normally, you do not need to configure this. See below for a list
of media types.
The `type` field can be used to select these mailer services:
- `mailgun`: Use Mailgun.
- `ses`: Use Amazon SES.
- `sendgrid`: Use SendGrid.
- `postmark`: Use Postmark.
- `twilio`: Use Twilio.
- `sns`: Use Amazon SNS.
It also supports these local mailers:
- `sendmail`: Use the local `sendmail` binary.
- `smtp`: Connect directly to an SMTP server.
- `test`: Internal mailer for testing. Does not send mail.
You can also write your own mailer by extending `PhabricatorMailAdapter`.
The `media` field supports these values:
- `email`: Configure this mailer for email.
- `sms`: Configure this mailer for SMS.
Once you've selected a mailer, find the corresponding section below for
instructions on configuring it.
Setting Complex Configuration
=============================
Mailers can not be edited from the web UI. If mailers could be edited from
the web UI, it would give an attacker who compromised an administrator account
a lot of power: they could redirect mail to a server they control and then
intercept mail for any other account, including password reset mail.
For more information about locked configuration options, see
@{article:Configuration Guide: Locked and Hidden Configuration}.
Setting `cluster.mailers` from the command line using `bin/config set` can be
tricky because of shell escaping. The easiest way to do it is to use the
`--stdin` flag. First, put your desired configuration in a file like this:
```lang=json, name=mailers.json
[
{
"key": "test-mailer",
"type": "test"
}
]
```
Then set the value like this:
```
phabricator/ $ ./bin/config set --stdin cluster.mailers < mailers.json
```
For alternatives and more information on configuration, see
@{article:Configuration User Guide: Advanced Configuration}
Mailer: Postmark
================
| Media | Email
|---------|
| Inbound | Yes
|---------|
Postmark is a third-party email delivery service. You can learn more at
<https://www.postmarkapp.com/>.
To use this mailer, set `type` to `postmark`, then configure these `options`:
- `access-token`: Required string. Your Postmark access token.
- `inbound-addresses`: Optional list<string>. Address ranges which you
will accept inbound Postmark HTTP webook requests from.
The default address list is preconfigured with Postmark's address range, so
you generally will not need to set or adjust it.
The option accepts a list of CIDR ranges, like `1.2.3.4/16` (IPv4) or
`::ffff:0:0/96` (IPv6). The default ranges are:
```lang=json
[
"50.31.156.6/32",
"50.31.156.77/32",
"18.217.206.57/32"
]
```
The default address ranges were last updated in January 2019, and were
documented at: <https://postmarkapp.com/support/article/800-ips-for-firewalls>
Mailer: Mailgun
===============
| Media | Email
|---------|
| Inbound | Yes
|---------|
Mailgun is a third-party email delivery service. You can learn more at
<https://www.mailgun.com>. Mailgun is easy to configure and works well.
To use this mailer, set `type` to `mailgun`, then configure these `options`:
- `api-key`: Required string. Your Mailgun API key.
- `domain`: Required string. Your Mailgun domain.
+ - `api-hostname`: Optional string. Defaults to "api.mailgun.net". If your
+ account is in another region (like the EU), you may need to specify a
+ different hostname. Consult the Mailgun documentation.
Mailer: Amazon SES
==================
| Media | Email
|---------|
| Inbound | No
|---------|
Amazon SES is Amazon's cloud email service. You can learn more at
<https://aws.amazon.com/ses/>.
To use this mailer, set `type` to `ses`, then configure these `options`:
- `access-key`: Required string. Your Amazon SES access key.
- `secret-key`: Required string. Your Amazon SES secret key.
- `endpoint`: Required string. Your Amazon SES endpoint.
NOTE: Amazon SES **requires you to verify your "From" address**. Configure
which "From" address to use by setting `metamta.default-address` in your
config, then follow the Amazon SES verification process to verify it. You
won't be able to send email until you do this!
Mailer: Twilio
==================
| Media | SMS
|---------|
| Inbound | No
|---------|
Twilio is a third-party notification service. You can learn more at
<https://www.twilio.com/>.
To use this mailer, set `type` to `twilio`, then configure these options:
- `account-sid`: Your Twilio Account SID.
- `auth-token`: Your Twilio Auth Token.
- `from-number`: Number to send text messages from, in E.164 format
(like `+15551237890`).
Mailer: Amazon SNS
==================
| Media | SMS
|---------|
| Inbound | No
|---------|
Amazon SNS is Amazon's cloud notification service. You can learn more at
<https://aws.amazon.com/sns/>. Note that this mailer is only able to send
SMS messages, not emails.
To use this mailer, set `type` to `sns`, then configure these options:
- `access-key`: Required string. Your Amazon SNS access key.
- `secret-key`: Required string. Your Amazon SNS secret key.
- `endpoint`: Required string. Your Amazon SNS endpoint.
- `region`: Required string. Your Amazon SNS region.
You can find the correct `region` value for your endpoint in the SNS
documentation.
Mailer: SendGrid
================
| Media | Email
|---------|
| Inbound | Yes
|---------|
SendGrid is a third-party email delivery service. You can learn more at
<https://sendgrid.com/>.
You can configure SendGrid in two ways: you can send via SMTP or via the REST
API. To use SMTP, configure Phabricator to use an `smtp` mailer.
To use the REST API mailer, set `type` to `sendgrid`, then configure
these `options`:
- `api-key`: Required string. Your SendGrid API key.
Older versions of the SendGrid API used different sets of credentials,
including an "API User". Make sure you're configuring your "API Key".
Mailer: Sendmail
================
| Media | Email
|---------|
| Inbound | Requires Configuration
|---------|
This requires a `sendmail` binary to be installed on the system. Most MTAs
(e.g., sendmail, qmail, postfix) should install one for you, but your machine
may not have one installed by default. For install instructions, consult the
documentation for your favorite MTA.
Since you'll be sending the mail yourself, you are subject to things like SPF
rules, blackholes, and MTA configuration which are beyond the scope of this
document. If you can already send outbound email from the command line or know
how to configure it, this option is straightforward. If you have no idea how to
do any of this, strongly consider using Postmark or Mailgun instead.
-To use this mailer, set `type` to `sendmail`. There are no `options` to
-configure.
+To use this mailer, set `type` to `sendmail`, then configure these `options`:
+ - `message-id`: Optional bool. Set to `false` if Phabricator will not be
+ able to select a custom "Message-ID" header when sending mail via this
+ mailer. See "Message-ID Headers" below.
Mailer: SMTP
============
| Media | Email
|---------|
| Inbound | Requires Configuration
|---------|
You can use this adapter to send mail via an external SMTP server, like Gmail.
To use this mailer, set `type` to `smtp`, then configure these `options`:
- `host`: Required string. The hostname of your SMTP server.
- `port`: Optional int. The port to connect to on your SMTP server.
- `user`: Optional string. Username used for authentication.
- `password`: Optional string. Password for authentication.
- `protocol`: Optional string. Set to `tls` or `ssl` if necessary. Use
`ssl` for Gmail.
+ - `message-id`: Optional bool. Set to `false` if Phabricator will not be
+ able to select a custom "Message-ID" header when sending mail via this
+ mailer. See "Message-ID Headers" below.
Disable Mail
============
| Media | All
|---------|
| Inbound | No
|---------|
To disable mail, just don't configure any mailers. (You can safely ignore the
setup warning reminding you to set up mailers if you don't plan to configure
any.)
Testing and Debugging Outbound Email
====================================
You can use the `bin/mail` utility to test, debug, and examine outbound mail. In
particular:
phabricator/ $ ./bin/mail list-outbound # List outbound mail.
phabricator/ $ ./bin/mail show-outbound # Show details about messages.
phabricator/ $ ./bin/mail send-test # Send test messages.
Run `bin/mail help <command>` for more help on using these commands.
By default, `bin/mail send-test` sends email messages, but you can use
the `--type` flag to send different types of messages.
You can monitor daemons using the Daemon Console (`/daemon/`, or click
**Daemon Console** from the homepage).
Priorities
==========
By default, Phabricator will try each mailer in order: it will try the first
mailer first. If that fails (for example, because the service is not available
at the moment) it will try the second mailer, and so on.
If you want to load balance between multiple mailers instead of using one as
a primary, you can set `priority`. Phabricator will start with mailers in the
highest priority group and go through them randomly, then fall back to the
next group.
For example, if you have two SMTP servers and you want to balance requests
between them and then fall back to Mailgun if both fail, configure priorities
like this:
```lang=json
[
{
"key": "smtp-uswest",
"type": "smtp",
"priority": 300,
"options": "..."
},
{
"key": "smtp-useast",
"type": "smtp",
"priority": 300,
"options": "..."
},
{
"key": "mailgun-fallback",
"type": "mailgun",
"options": "..."
}
}
```
Phabricator will start with servers in the highest priority group (the group
with the **largest** `priority` number). In this example, the highest group is
`300`, which has the two SMTP servers. They'll be tried in random order first.
If both fail, Phabricator will move on to the next priority group. In this
example, there are no other priority groups.
If it still hasn't sent the mail, Phabricator will try servers which are not
in any priority group, in the configured order. In this example there is
only one such server, so it will try to send via Mailgun.
+Message-ID Headers
+==================
+
+Email has a "Message-ID" header which is important for threading messages
+correctly in mail clients. Normally, Phabricator is free to select its own
+"Message-ID" header values for mail it sends.
+
+However, some mailers (including Amazon SES) do not allow selection of custom
+"Message-ID" values and will ignore or replace the "Message-ID" in mail that
+is submitted through them.
+
+When Phabricator adds other mail headers which affect threading, like
+"In-Reply-To", it needs to know if its "Message-ID" headers will be respected
+or not to select header values which will produce good threading behavior. If
+we guess wrong and think we can set a "Message-ID" header when we can't, you
+may get poor threading behavior in mail clients.
+
+For most mailers (like Postmark, Mailgun, and Amazon SES), the correct setting
+will be selected for you automatically, because the behavior of the mailer
+is knowable ahead of time. For example, we know Amazon SES will never respect
+our "Message-ID" headers.
+
+However, if you're sending mail indirectly through a mailer like SMTP or
+Sendmail, the mail might or might not be routing through some mail service
+which will ignore or replace the "Message-ID" header.
+
+For example, your local mailer might submit mail to Mailgun (so "Message-ID"
+will work), or to Amazon SES (so "Message-ID" will not work), or to some other
+mail service (which we may not know anything about). We can't make a reliable
+guess about whether "Message-ID" will be respected or not based only on
+the local mailer configuration.
+
+By default, we check if the mailer has a hostname we recognize as belonging
+to a service which does not allow us to set a "Message-ID" header. If we don't
+recognize the hostname (which is very common, since these services are most
+often configured against the localhost or some other local machine), we assume
+we can set a "Message-ID" header.
+
+If the outbound pathway does not actually allow selection of a "Message-ID"
+header, you can set the `message-id` option on the mailer to `false` to tell
+Phabricator that it should not assume it can select a value for this header.
+
+For example, if you are sending mail via a local Postfix server which then
+forwards the mail to Amazon SES (a service which does not allow selection of
+a "Message-ID" header), your `smtp` configuration in Phabricator should
+specify `"message-id": false`.
+
+
Next Steps
==========
Continue by:
- @{article:Configuring Inbound Email} so users can reply to email they
receive about revisions and tasks to interact with them; or
- learning about daemons with @{article:Managing Daemons with phd}; or
- returning to the @{article:Configuration Guide}.
diff --git a/src/docs/user/userguide/audit.diviner b/src/docs/user/userguide/audit.diviner
index 0d1086790..5223f6c96 100644
--- a/src/docs/user/userguide/audit.diviner
+++ b/src/docs/user/userguide/audit.diviner
@@ -1,202 +1,199 @@
@title Audit User Guide
@group userguide
Guide to using Phabricator to audit published commits.
Overview
========
Phabricator supports two code review workflows, "review" (pre-publish) and
"audit" (post-publish). To understand the differences between the two, see
@{article:User Guide: Review vs Audit}.
How Audit Works
===============
The audit workflow occurs after changes have been published. It provides ways
to track, discuss, and resolve issues with commits that are discovered after
they go through whatever review process you have in place (if you have one).
Two examples of how you might use audit are:
**Fix Issues**: If a problem is discovered after a change has already been
published, users can find the commit which introduced the problem and raise a
concern on it. This notifies the author of the commit and prompts them to
remedy the issue.
**Watch Changes**: In some cases, you may want to passively look over changes
that satisfy some criteria as they are published. For example, you may want to
review all Javascript changes at the end of the week to keep an eye on things,
or make sure that code which impacts a subsystem is looked at by someone on
that team, eventually.
Developers may also want other developers to take a second look at things if
they realize they aren't sure about something after a change has been published,
or just want to provide a heads-up.
You can configure Herald rules and Owners packages to automatically trigger
audits of commits that satisfy particular criteria.
Audit States and Actions
========================
The audit workflow primarily keeps track of two things:
- **Commits** and their audit state (like "Not Audited", "Approved", or
"Concern Raised").
- **Audit Requests** which ask a user (or some other entity, like a project
or package) to audit a commit. These can be triggered in a number of ways
(see below).
Users interact with commits by leaving comments and applying actions, like
accepting the changes or raising a concern. These actions change the state of
their own audit and the overall audit state of the commit. Here's an example of
a typical audit workflow:
- Alice publishes a commit containing some Javascript.
- This triggers an audit request to Bailey, the Javascript technical
lead on the project (see below for a description of trigger mechanisms).
- Later, Bailey logs into Phabricator and sees the audit request. She ignores
it for the moment, since it isn't blocking anything. At the end of the
week she looks through her open requests to see what the team has been
up to.
- Bailey notices a few minor problems with Alice's commit. She leaves
comments describing improvements and uses "Raise Concern" to send the
commit back into Alice's queue.
- Later, Alice logs into Phabricator and sees that Bailey has raised a
concern (usually, Alice will also get an email). She resolves the issue
somehow, maybe by making a followup commit with fixes.
- After the issues have been dealt with, she uses "Request Verification" to
return the change to Bailey so Bailey can verify that the concerns have
been addressed.
- Bailey uses "Accept Commit" to close the audit.
In {nav Diffusion > Browse Commits}, you can review commits and query for
commits with certain audit states. The default "Active Audits" view shows
all of the commits which are relevant to you given their audit state, divided
into buckets:
- **Needs Attention**: These are commits which you authored that another
user has raised a concern about: for example, maybe they believe they have
found a bug or some other problem. You should address the concerns.
- **Needs Verification**: These are commits which someone else authored
that you previously raised a concern about. The author has indicated that
they believe the concern has been addressed. You should verify that the
remedy is satisfactory and accept the change, or raise a further concern.
- **Ready to Audit**: These are commits which someone else authored that you
have been asked to audit, either by a user or by a system rule. You should
look over the changes and either accept them or raise concerns.
- **Waiting on Authors**: These are commits which someone else authored that
you previously raised a concern about. The author has not responded to the
concern yet. You may want to follow up.
- **Waiting on Auditors**: These are commits which you authored that someone
else needs to audit.
You can use the query constraints to filter this list or find commits that
match certain criteria.
Audit Triggers
==============
Audit requests can be triggered in a number of ways:
- You can add auditors explicitly from the web UI, using either "Edit Commit"
or the "Change Auditors" action. You might do this if you realize you are
not sure about something that you recently published and want a second
opinion.
- If you put `Auditors: username1, username2` in your commit message, it will
trigger an audit request to those users when you push it to a tracked
branch.
- You can create rules in Herald that trigger audits based on properties
of the commit -- like the files it touches, the text of the change, the
author, etc.
- You can create an Owners package and enable automatic auditing for the
package.
Audits in Small Teams
=====================
If you have a small team and don't need complicated trigger rules, you can set
up a simple audit workflow like this:
- Create a new Project, "Code Audits".
- Create a new global Herald rule for Commits, which triggers an audit by
the "Code Audits" project for every commit where "Differential Revision"
"does not exist" (this will allow you to transition partly or fully to
review later if you want).
- Have every engineer join the "Code Audits" project.
This way, everyone will see an audit request for every commit, but it will be
dismissed if anyone approves it. Effectively, this enforces the rule "every
commit should have //someone// look at it".
Once your team gets bigger, you can refine this ruleset so that developers see
only changes that are relevant to them.
Audit Tips
==========
- When viewing a commit, audit requests you are responsible for are
highlighted. You are responsible for a request if it's a user request
and you're that user, or if it's a project request and you're a member
of the project, or if it's a package request and you're a package owner.
Any action you take will update the state of all the requests you're
responsible for.
- You can leave inline comments by clicking the line numbers in the diff.
- You can leave a comment across multiple lines by dragging across the line
numbers.
- Inline comments are initially saved as drafts. They are not submitted until
you submit a comment at the bottom of the page.
- Press "?" to view keyboard shortcuts.
Audit Maintenance
=================
The `bin/audit` command allows you to perform several maintenance operations.
Get more information about a command by running:
```
phabricator/ $ ./bin/audit help <command>
```
Supported operations are:
**Delete Audits**: Delete audits that match certain parameters with
`bin/audit delete`.
You can use this command to forcibly delete requests which may have triggered
incorrectly (for example, because a package or Herald rule was configured in an
overbroad way).
-After deleting audits, you may want to run `bin/audit synchronize` to
-synchronize audit state.
-
**Synchronize Audit State**: Synchronize the audit state of commits to the
current open audit requests with `bin/audit synchronize`.
Normally, overall audit state is automatically kept up to date as changes are
-made to an audit. However, if you delete audits or manually update the database
-to make changes to audit request state, the state of corresponding commits may
-no longer be correct.
+made to an audit. However, if you manually update the database to make changes
+to audit request state, the state of corresponding commits may no longer be
+consistent.
This command will update commits so their overall audit state reflects the
cumulative state of their actual audit requests.
**Update Owners Package Membership**: Update which Owners packages commits
belong to with `bin/audit update-owners`.
Normally, commits are automatically associated with packages when they are
imported. You can use this command to manually rebuild this association if
you run into problems with it.
Next Steps
==========
- Learn more about Herald at @{article:Herald User Guide}.
diff --git a/src/docs/user/userguide/differential_faq.diviner b/src/docs/user/userguide/differential_faq.diviner
index aea49c4ce..0df1f7b1c 100644
--- a/src/docs/user/userguide/differential_faq.diviner
+++ b/src/docs/user/userguide/differential_faq.diviner
@@ -1,80 +1,64 @@
@title Differential User Guide: FAQ
@group userguide
Common questions about Differential.
= Why does an "accepted" revision remain accepted when it is updated? =
You can configure this behavior with `differential.sticky-accept`.
When a revision author updates an "Accepted" revision in Differential, the
state remains "Accepted". This can be confusing if you expect the revision to
change to "Needs Review" when it is updated.
Although this behavior is configurable, we think stickiness is a good behavior:
stickiness encourage authors to update revisions when they make minor changes
after a revision is accepted. For example, a reviewer may accept a change with a
comment like this:
> Looks great, but can you add some documentation for the foo() function
> before you land it? I also caught a couple typos, see inlines.
If updating the revision reverted the status to "Needs Review", the author
is discouraged from updating the revision when they make minor changes because
they'll have to wait for their reviewer to have a chance to look at it again.
Instead, the "Accepted" state is sticky to encourage them to update the revision
with a comment like:
> - Added docs.
> - Fixed typos.
This makes it much easier for the reviewer to go double-check those changes
later if they want, and the update tells them that the author acknowledged their
suggestions even if they don't bother to go double-check them.
If an author makes significant changes and wants to get them looked at, they can
always "request review" of an accepted revision, with a comment like:
> When I was testing my typo fix, I realized I actually had a bug, so I had to
> make some more changes to the bar() implementation -- can you look them over?
If authors are being jerks about this (making sweeping changes as soon as they
get an accept), solve the problem socially by telling them to stop being jerks.
Unless you've configured additional layers of enforcement, there's nothing
stopping them from silently changing the code before pushing it, anyway.
= How can I enable syntax highlighting? =
You need to install and configure **Pygments** to highlight anything else than
PHP. See the `pygments.enabled` configuration setting.
-= What do the whitespace options mean? =
-
-Most of these are pretty straightforward, but "Ignore Most" is not:
-
- - **Show All**: Show all whitespace.
- - **Ignore Trailing**: Ignore changes which only affect trailing whitespace.
- - **Ignore Most**: Ignore changes which only affect leading or trailing
- whitespace (but not whitespace changes between non-whitespace characters)
- in files which are not marked as having significant whitespace.
- In those files, show whitespace changes. By default, Python (.py) and
- Haskell (.lhs, .hs) are marked as having significant whitespace, but this
- can be changed in the `differential.whitespace-matters` configuration
- setting.
- - **Ignore All**: Ignore all whitespace changes in all files.
-
-
= What do the very light green and red backgrounds mean? =
Differential uses these colors to mark changes coming from rebase: they are
part of the diff but they were not added or removed by the author. They can
appear in diff of diffs against different bases.
= Next Steps =
Continue by:
- returning to the @{article:Differential User Guide}.
diff --git a/src/docs/user/userguide/diviner.diviner b/src/docs/user/userguide/diviner.diviner
index e94c33d27..01484be14 100644
--- a/src/docs/user/userguide/diviner.diviner
+++ b/src/docs/user/userguide/diviner.diviner
@@ -1,84 +1,95 @@
@title Diviner User Guide
@group userguide
Using Diviner, a documentation generator.
-= Overview =
+Overview
+========
-NOTE: Diviner is new and not yet generally useful.
+Diviner is an application for creating technical documentation.
-= Generating Documentation =
+This article is maintained in a text file in the Phabricator repository and
+generated into the display document you are currently reading using Diviner.
+
+Beyond generating articles, Diviner can also analyze source code and generate
+documentation about classes, methods, and other primitives.
+
+
+Generating Documentation
+========================
To generate documentation, run:
phabricator/ $ ./bin/diviner generate --book <book>
-= .book Files =
+
+Diviner ".book" Files
+=====================
Diviner documentation books are configured using JSON `.book` files, which
look like this:
name=example.book
{
"name" : "example",
"title" : "Example Documentation",
"short" : "Example Docs",
"root" : ".",
"uri.source" : "http://example.com/diffusion/X/browse/master/%f$%l",
"rules" : {
"(\\.diviner$)" : "DivinerArticleAtomizer"
},
"exclude" : [
"(^externals/)",
"(^scripts/)",
"(^support/)"
],
"groups" : {
"forward" : {
"name" : "Doing Stuff"
},
"reverse" : {
"name" : "Undoing Stuff"
}
}
}
The properties in this file are:
- `name`: Required. Short, unique name to identify the documentation book.
This will be used in URIs, so it should not have special characters. Good
names are things like `"example"` or `"libcabin"`.
- `root`: Required. The root directory (relative to the `.book` file) which
documentation should be generated from. Often this will be a value like
`"../../"`, to specify the project root (for example, if the `.book` file
is in `project/src/docs/example.book`, the value `"../../"` would generate
documentation from the `project/` directory.
- `title`: Optional. Full human-readable title of the documentation book. This
is used when there's plenty of display space and should completely describe
the book. Good titles are things like `"Example Documentation"`, or
`"libcabin Developer Documentation"`.
- `short`: Optional. Shorter version of the title for use when display space
is limited (for example, in navigation breadcrumbs). If omitted, the full
title is used. Good short titles are things like `"Example Docs"` or
`"libcabin Dev Docs"`.
- `uri.source`: Optional. Diviner can link from the documentation to a
repository browser so that you can quickly jump to the definition of a class
or function. To do this, it uses a URI pattern which you specify here.
Normally, this URI should point at a repository browser like Diffusion.
For example, `"http://repobrowser.yourcompany.com/%f#%l"`. You can use these
conversions in the URI, which will be replaced at runtime:
- `%f`: Replaced with the name of the file.
- `%l`: Replaced with the line number.
- `%%`: Replaced with a literal `%` symbol.
- `rules`: Optional. A map of regular expressions to Atomizer classes which
controls which documentation generator runs on each file. If omitted,
Diviner will use its default ruleset. For example, adding the key
`"(\\.diviner$)"` to the map with value `"DivinerArticleAtomizer"` tells
Diviner to analyze any file with a name ending in `.diviner` using the
"article" atomizer.
- `exclude`: Optional. A list of regular expressions matching paths which
will be excluded from documentation generation for this book. For example,
adding a pattern like `"(^externals/)"` or `"(^vendor/)"` will make Diviner
ignore those directories.
- `groups`: Optional. Describes top level organizational groups which atoms
should be placed into.
diff --git a/src/docs/user/userguide/owners.diviner b/src/docs/user/userguide/owners.diviner
index 95a388255..11dee8941 100644
--- a/src/docs/user/userguide/owners.diviner
+++ b/src/docs/user/userguide/owners.diviner
@@ -1,174 +1,181 @@
@title Owners User Guide
@group userguide
Group files in a codebase into packages and define ownership.
Overview
========
The Owners application allows you to group files in a codebase (or across
codebases) into packages. This can make it easier to reference a module or
subsystem in other applications, like Herald.
Creating a Package
==================
To create a package, choose a name and add some files which belong to the
package. For example, you might define an "iOS Application" package by
including these paths:
/conf/ios/
/src/ios/
/shared/assets/mobile/
Any files in those directories are considered to be part of the package, and
you can now conveniently refer to them (for example, in a Herald rule) by
referring to the package instead of copy/pasting a huge regular expression
into a bunch of places.
If new source files are later added, or the scope of the package otherwise
expands or contracts, you can edit the package definition to keep things
updated.
You can use "exclude" paths to ignore subdirectories which would otherwise
be considered part of the package. For example, you might exclude a path
like this:
/conf/ios/generated/
Perhaps that directory contains some generated configuration which frequently
changes, and which you aren't concerned about.
After creating a package, files the package contains will be identified as
belonging to the package when you look at them in Diffusion, or look at changes
which affect them in Diffusion or Differential.
Dominion
========
The **Dominion** option allows you to control how ownership cascades when
multiple packages own a path. The dominion rules are:
**Strong Dominion.** This is the default. In this mode, the package will always
own all files matching its configured paths, even if another package also owns
them.
For example, if the package owns `a/`, it will always own `a/b/c.z` even if
another package owns `a/b/`. In this case, both packages will own `a/b/c.z`.
This mode prevents users from stealing files away from the package by defining
more narrow ownership rules in new packages, but enforces hierarchical
ownership rules.
**Weak Dominion.** In this mode, the package will only own files which do not
match a more specific path in another package.
For example, if the package owns `a/` but another package owns `a/b/`, the
package will no longer consider `a/b/c.z` to be a file it owns because another
package matches the path with a more specific rule.
This mode lets you to define rules without implicit hierarchical ownership,
but allows users to steal files away from a package by defining a more
specific package.
For more details on files which match multiple packages, see
"Files in Multiple Packages", below.
Auto Review
===========
You can configure **Auto Review** for packages. When a new code review is
created in Differential which affects code in a package, the package can
automatically be added as a subscriber or reviewer.
The available settings allow you to take these actions:
- **Review Changes**: This package will be added to reviews as a reviewer.
Reviews will appear on the dashboards of package owners.
- **Review Changes (Blocking)** This package will be added to reviews as a
blocking reviewer. A package owner will be required to accept changes
before they may land.
- **Subscribe to Changes**: This package will be added to reviews as a
subscriber. Owners will be notified of changes, but not required to act.
If you select the **With Non-Owner Author** option for these actions, the
action will not trigger if the author of the revision is a package owner. This
mode may be helpful if you are using Owners mostly to make sure that someone
who is qualified is involved in each change to a piece of code.
If you select the **All** option for these actions, the action will always
trigger even if the author is a package owner. This mode may be helpful if you
are using Owners mostly to suggest reviewers.
These rules do not trigger if the package has been archived.
The intent of this feature is to make it easy to configure simple, reasonable
behaviors. If you want more tailored or specific triggers, you can write more
powerful rules by using Herald.
Auditing
========
You can automatically trigger audits on unreviewed code by configuring
-**Auditing**. The available settings are:
+**Auditing**. The available settings allow you to select behavior based on
+these conditions:
- - **Disabled**: Do not trigger audits.
- - **Enabled**: Trigger audits.
+ - **No Owner Involvement**: Triggers an audit when the commit author is not
+ a package owner, and no package owner reviewed an associated revision in
+ Differential.
+ - **Unreviewed Commits**: Triggers an audit when a commit has no associated
+ revision in Differential, or the associated revision in Differential landed
+ without being "Accepted".
-When enabled, audits are triggered for commits which:
+For example, the **Audit Commits With No Owner Involvement** option triggers
+audits for commits which:
- affect code owned by the package;
- were not authored by a package owner; and
- - were not accepted by a package owner.
+ - were not accepted (in Differential) by a package owner or the package
+ itself.
Audits do not trigger if the package has been archived.
The intent of this feature is to make it easy to configure simple auditing
behavior. If you want more powerful auditing behavior, you can use Herald to
write more sophisticated rules.
Ignored Attributes
==================
You can automatically exclude certain types of files, like generated files,
with **Ignored Attributes**.
When a package is marked as ignoring files with a particular attribute, and
a file in a particular change has that attribute, the file will be ignored when
computing ownership.
(This feature is currently rough, only works for Differential revisions, and
may not always compute the correct set of owning packages in some complex
cases where it interacts with dominion rules.)
Files in Multiple Packages
==========================
Multiple packages may own the same file. For example, both the
"Android Application" and the "iOS Application" packages might own a path
like this, containing resources used by both:
/shared/assets/mobile/
If both packages own this directory, files in the directory are considered to
be part of both packages.
Packages do not need to have claims of equal specificity to own files. For
example, if you have a "Design Assets" package which owns this path:
/shared/assets/
...it will //also// own all of the files in the `mobile/` subdirectory. In this
configuration, these files are part of three packages: "iOS Application",
"Android Application", and "Design Assets".
(You can use an "exclude" rule if you want to make a different package with a
more specific claim the owner of a file or subdirectory. You can also change
the **Dominion** setting for a package to let it give up ownership of paths
owned by another package.)
diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php
index d7df3c5b7..c6c70a961 100644
--- a/src/infrastructure/customfield/field/PhabricatorCustomField.php
+++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php
@@ -1,1625 +1,1712 @@
<?php
/**
* @task apps Building Applications with Custom Fields
* @task core Core Properties and Field Identity
* @task proxy Field Proxies
* @task context Contextual Data
* @task render Rendering Utilities
* @task storage Field Storage
* @task edit Integration with Edit Views
* @task view Integration with Property Views
* @task list Integration with List views
* @task appsearch Integration with ApplicationSearch
* @task appxaction Integration with ApplicationTransactions
* @task xactionmail Integration with Transaction Mail
* @task globalsearch Integration with Global Search
* @task herald Integration with Herald
*/
abstract class PhabricatorCustomField extends Phobject {
private $viewer;
private $object;
private $proxy;
const ROLE_APPLICATIONTRANSACTIONS = 'ApplicationTransactions';
const ROLE_TRANSACTIONMAIL = 'ApplicationTransactions.mail';
const ROLE_APPLICATIONSEARCH = 'ApplicationSearch';
const ROLE_STORAGE = 'storage';
const ROLE_DEFAULT = 'default';
const ROLE_EDIT = 'edit';
const ROLE_VIEW = 'view';
const ROLE_LIST = 'list';
const ROLE_GLOBALSEARCH = 'GlobalSearch';
const ROLE_CONDUIT = 'conduit';
const ROLE_HERALD = 'herald';
const ROLE_EDITENGINE = 'EditEngine';
const ROLE_HERALDACTION = 'herald.action';
const ROLE_EXPORT = 'export';
/* -( Building Applications with Custom Fields )--------------------------- */
/**
* @task apps
*/
public static function getObjectFields(
PhabricatorCustomFieldInterface $object,
$role) {
try {
$attachment = $object->getCustomFields();
} catch (PhabricatorDataNotAttachedException $ex) {
$attachment = new PhabricatorCustomFieldAttachment();
$object->attachCustomFields($attachment);
}
try {
$field_list = $attachment->getCustomFieldList($role);
} catch (PhabricatorCustomFieldNotAttachedException $ex) {
$base_class = $object->getCustomFieldBaseClass();
$spec = $object->getCustomFieldSpecificationForRole($role);
if (!is_array($spec)) {
throw new Exception(
pht(
"Expected an array from %s for object of class '%s'.",
'getCustomFieldSpecificationForRole()',
get_class($object)));
}
$fields = self::buildFieldList(
$base_class,
$spec,
$object);
+ $fields = self::adjustCustomFieldsForObjectSubtype(
+ $object,
+ $role,
+ $fields);
+
foreach ($fields as $key => $field) {
+ // NOTE: We perform this filtering in "buildFieldList()", but may need
+ // to filter again after subtype adjustment.
+ if (!$field->isFieldEnabled()) {
+ unset($fields[$key]);
+ continue;
+ }
+
if (!$field->shouldEnableForRole($role)) {
unset($fields[$key]);
+ continue;
}
}
foreach ($fields as $field) {
$field->setObject($object);
}
$field_list = new PhabricatorCustomFieldList($fields);
$attachment->addCustomFieldList($role, $field_list);
}
return $field_list;
}
/**
* @task apps
*/
public static function getObjectField(
PhabricatorCustomFieldInterface $object,
$role,
$field_key) {
$fields = self::getObjectFields($object, $role)->getFields();
return idx($fields, $field_key);
}
/**
* @task apps
*/
public static function buildFieldList(
$base_class,
array $spec,
$object,
array $options = array()) {
$field_objects = id(new PhutilClassMapQuery())
->setAncestorClass($base_class)
->execute();
$fields = array();
foreach ($field_objects as $field_object) {
$field_object = clone $field_object;
foreach ($field_object->createFields($object) as $field) {
$key = $field->getFieldKey();
if (isset($fields[$key])) {
throw new Exception(
pht(
"Both '%s' and '%s' define a custom field with ".
"field key '%s'. Field keys must be unique.",
get_class($fields[$key]),
get_class($field),
$key));
}
$fields[$key] = $field;
}
}
foreach ($fields as $key => $field) {
if (!$field->isFieldEnabled()) {
unset($fields[$key]);
}
}
$fields = array_select_keys($fields, array_keys($spec)) + $fields;
if (empty($options['withDisabled'])) {
foreach ($fields as $key => $field) {
if (isset($spec[$key]['disabled'])) {
$is_disabled = $spec[$key]['disabled'];
} else {
$is_disabled = $field->shouldDisableByDefault();
}
if ($is_disabled) {
if ($field->canDisableField()) {
unset($fields[$key]);
}
}
}
}
return $fields;
}
/* -( Core Properties and Field Identity )--------------------------------- */
/**
* Return a key which uniquely identifies this field, like
* "mycompany:dinosaur:count". Normally you should provide some level of
* namespacing to prevent collisions.
*
* @return string String which uniquely identifies this field.
* @task core
*/
public function getFieldKey() {
if ($this->proxy) {
return $this->proxy->getFieldKey();
}
throw new PhabricatorCustomFieldImplementationIncompleteException(
$this,
$field_key_is_incomplete = true);
}
public function getModernFieldKey() {
if ($this->proxy) {
return $this->proxy->getModernFieldKey();
}
return $this->getFieldKey();
}
/**
* Return a human-readable field name.
*
* @return string Human readable field name.
* @task core
*/
public function getFieldName() {
if ($this->proxy) {
return $this->proxy->getFieldName();
}
return $this->getModernFieldKey();
}
/**
* Return a short, human-readable description of the field's behavior. This
* provides more context to administrators when they are customizing fields.
*
* @return string|null Optional human-readable description.
* @task core
*/
public function getFieldDescription() {
if ($this->proxy) {
return $this->proxy->getFieldDescription();
}
return null;
}
/**
* Most field implementations are unique, in that one class corresponds to
* one field. However, some field implementations are general and a single
* implementation may drive several fields.
*
* For general implementations, the general field implementation can return
* multiple field instances here.
*
* @param object The object to create fields for.
* @return list<PhabricatorCustomField> List of fields.
* @task core
*/
public function createFields($object) {
return array($this);
}
/**
* You can return `false` here if the field should not be enabled for any
* role. For example, it might depend on something (like an application or
* library) which isn't installed, or might have some global configuration
* which allows it to be disabled.
*
* @return bool False to completely disable this field for all roles.
* @task core
*/
public function isFieldEnabled() {
if ($this->proxy) {
return $this->proxy->isFieldEnabled();
}
return true;
}
/**
* Low level selector for field availability. Fields can appear in different
* roles (like an edit view, a list view, etc.), but not every field needs
* to appear everywhere. Fields that are disabled in a role won't appear in
* that context within applications.
*
* Normally, you do not need to override this method. Instead, override the
* methods specific to roles you want to enable. For example, implement
* @{method:shouldUseStorage()} to activate the `'storage'` role.
*
* @return bool True to enable the field for the given role.
* @task core
*/
public function shouldEnableForRole($role) {
// NOTE: All of these calls proxy individually, so we don't need to
// proxy this call as a whole.
switch ($role) {
case self::ROLE_APPLICATIONTRANSACTIONS:
return $this->shouldAppearInApplicationTransactions();
case self::ROLE_APPLICATIONSEARCH:
return $this->shouldAppearInApplicationSearch();
case self::ROLE_STORAGE:
return $this->shouldUseStorage();
case self::ROLE_EDIT:
return $this->shouldAppearInEditView();
case self::ROLE_VIEW:
return $this->shouldAppearInPropertyView();
case self::ROLE_LIST:
return $this->shouldAppearInListView();
case self::ROLE_GLOBALSEARCH:
return $this->shouldAppearInGlobalSearch();
case self::ROLE_CONDUIT:
return $this->shouldAppearInConduitDictionary();
case self::ROLE_TRANSACTIONMAIL:
return $this->shouldAppearInTransactionMail();
case self::ROLE_HERALD:
return $this->shouldAppearInHerald();
case self::ROLE_HERALDACTION:
return $this->shouldAppearInHeraldActions();
case self::ROLE_EDITENGINE:
return $this->shouldAppearInEditView() ||
$this->shouldAppearInEditEngine();
case self::ROLE_EXPORT:
return $this->shouldAppearInDataExport();
case self::ROLE_DEFAULT:
return true;
default:
throw new Exception(pht("Unknown field role '%s'!", $role));
}
}
/**
* Allow administrators to disable this field. Most fields should allow this,
* but some are fundamental to the behavior of the application and can be
* locked down to avoid chaos, disorder, and the decline of civilization.
*
* @return bool False to prevent this field from being disabled through
* configuration.
* @task core
*/
public function canDisableField() {
return true;
}
public function shouldDisableByDefault() {
return false;
}
/**
* Return an index string which uniquely identifies this field.
*
* @return string Index string which uniquely identifies this field.
* @task core
*/
final public function getFieldIndex() {
return PhabricatorHash::digestForIndex($this->getFieldKey());
}
/* -( Field Proxies )------------------------------------------------------ */
/**
* Proxies allow a field to use some other field's implementation for most
* of their behavior while still subclassing an application field. When a
* proxy is set for a field with @{method:setProxy}, all of its methods will
* call through to the proxy by default.
*
* This is most commonly used to implement configuration-driven custom fields
* using @{class:PhabricatorStandardCustomField}.
*
* This method must be overridden to return `true` before a field can accept
* proxies.
*
* @return bool True if you can @{method:setProxy} this field.
* @task proxy
*/
public function canSetProxy() {
if ($this instanceof PhabricatorStandardCustomFieldInterface) {
return true;
}
return false;
}
/**
* Set the proxy implementation for this field. See @{method:canSetProxy} for
* discussion of field proxies.
*
* @param PhabricatorCustomField Field implementation.
* @return this
*/
final public function setProxy(PhabricatorCustomField $proxy) {
if (!$this->canSetProxy()) {
throw new PhabricatorCustomFieldNotProxyException($this);
}
$this->proxy = $proxy;
return $this;
}
/**
* Get the field's proxy implementation, if any. For discussion, see
* @{method:canSetProxy}.
*
* @return PhabricatorCustomField|null Proxy field, if one is set.
*/
final public function getProxy() {
return $this->proxy;
}
/* -( Contextual Data )---------------------------------------------------- */
/**
* Sets the object this field belongs to.
*
* @param PhabricatorCustomFieldInterface The object this field belongs to.
* @return this
* @task context
*/
final public function setObject(PhabricatorCustomFieldInterface $object) {
if ($this->proxy) {
$this->proxy->setObject($object);
return $this;
}
$this->object = $object;
$this->didSetObject($object);
return $this;
}
/**
* Read object data into local field storage, if applicable.
*
* @param PhabricatorCustomFieldInterface The object this field belongs to.
* @return this
* @task context
*/
public function readValueFromObject(PhabricatorCustomFieldInterface $object) {
if ($this->proxy) {
$this->proxy->readValueFromObject($object);
}
return $this;
}
/**
* Get the object this field belongs to.
*
* @return PhabricatorCustomFieldInterface The object this field belongs to.
* @task context
*/
final public function getObject() {
if ($this->proxy) {
return $this->proxy->getObject();
}
return $this->object;
}
/**
* This is a hook, primarily for subclasses to load object data.
*
* @return PhabricatorCustomFieldInterface The object this field belongs to.
* @return void
*/
protected function didSetObject(PhabricatorCustomFieldInterface $object) {
return;
}
/**
* @task context
*/
final public function setViewer(PhabricatorUser $viewer) {
if ($this->proxy) {
$this->proxy->setViewer($viewer);
return $this;
}
$this->viewer = $viewer;
return $this;
}
/**
* @task context
*/
final public function getViewer() {
if ($this->proxy) {
return $this->proxy->getViewer();
}
return $this->viewer;
}
/**
* @task context
*/
final protected function requireViewer() {
if ($this->proxy) {
return $this->proxy->requireViewer();
}
if (!$this->viewer) {
throw new PhabricatorCustomFieldDataNotAvailableException($this);
}
return $this->viewer;
}
/* -( Rendering Utilities )------------------------------------------------ */
/**
* @task render
*/
protected function renderHandleList(array $handles) {
if (!$handles) {
return null;
}
$out = array();
foreach ($handles as $handle) {
$out[] = $handle->renderHovercardLink();
}
return phutil_implode_html(phutil_tag('br'), $out);
}
/* -( Storage )------------------------------------------------------------ */
/**
* Return true to use field storage.
*
* Fields which can be edited by the user will most commonly use storage,
* while some other types of fields (for instance, those which just display
* information in some stylized way) may not. Many builtin fields do not use
* storage because their data is available on the object itself.
*
* If you implement this, you must also implement @{method:getValueForStorage}
* and @{method:setValueFromStorage}.
*
* @return bool True to use storage.
* @task storage
*/
public function shouldUseStorage() {
if ($this->proxy) {
return $this->proxy->shouldUseStorage();
}
return false;
}
/**
* Return a new, empty storage object. This should be a subclass of
* @{class:PhabricatorCustomFieldStorage} which is bound to the application's
* database.
*
* @return PhabricatorCustomFieldStorage New empty storage object.
* @task storage
*/
public function newStorageObject() {
// NOTE: This intentionally isn't proxied, to avoid call cycles.
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Return a serialized representation of the field value, appropriate for
* storing in auxiliary field storage. You must implement this method if
* you implement @{method:shouldUseStorage}.
*
* If the field value is a scalar, it can be returned unmodiifed. If not,
* it should be serialized (for example, using JSON).
*
* @return string Serialized field value.
* @task storage
*/
public function getValueForStorage() {
if ($this->proxy) {
return $this->proxy->getValueForStorage();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Set the field's value given a serialized storage value. This is called
* when the field is loaded; if no data is available, the value will be
* null. You must implement this method if you implement
* @{method:shouldUseStorage}.
*
* Usually, the value can be loaded directly. If it isn't a scalar, you'll
* need to undo whatever serialization you applied in
* @{method:getValueForStorage}.
*
* @param string|null Serialized field representation (from
* @{method:getValueForStorage}) or null if no value has
* ever been stored.
* @return this
* @task storage
*/
public function setValueFromStorage($value) {
if ($this->proxy) {
return $this->proxy->setValueFromStorage($value);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
public function didSetValueFromStorage() {
if ($this->proxy) {
return $this->proxy->didSetValueFromStorage();
}
return $this;
}
/* -( ApplicationSearch )-------------------------------------------------- */
/**
* Appearing in ApplicationSearch allows a field to be indexed and searched
* for.
*
* @return bool True to appear in ApplicationSearch.
* @task appsearch
*/
public function shouldAppearInApplicationSearch() {
if ($this->proxy) {
return $this->proxy->shouldAppearInApplicationSearch();
}
return false;
}
/**
* Return one or more indexes which this field can meaningfully query against
* to implement ApplicationSearch.
*
* Normally, you should build these using @{method:newStringIndex} and
* @{method:newNumericIndex}. For example, if a field holds a numeric value
* it might return a single numeric index:
*
* return array($this->newNumericIndex($this->getValue()));
*
* If a field holds a more complex value (like a list of users), it might
* return several string indexes:
*
* $indexes = array();
* foreach ($this->getValue() as $phid) {
* $indexes[] = $this->newStringIndex($phid);
* }
* return $indexes;
*
* @return list<PhabricatorCustomFieldIndexStorage> List of indexes.
* @task appsearch
*/
public function buildFieldIndexes() {
if ($this->proxy) {
return $this->proxy->buildFieldIndexes();
}
return array();
}
/**
* Return an index against which this field can be meaningfully ordered
* against to implement ApplicationSearch.
*
* This should be a single index, normally built using
* @{method:newStringIndex} and @{method:newNumericIndex}.
*
* The value of the index is not used.
*
* Return null from this method if the field can not be ordered.
*
* @return PhabricatorCustomFieldIndexStorage A single index to order by.
* @task appsearch
*/
public function buildOrderIndex() {
if ($this->proxy) {
return $this->proxy->buildOrderIndex();
}
return null;
}
/**
* Build a new empty storage object for storing string indexes. Normally,
* this should be a concrete subclass of
* @{class:PhabricatorCustomFieldStringIndexStorage}.
*
* @return PhabricatorCustomFieldStringIndexStorage Storage object.
* @task appsearch
*/
protected function newStringIndexStorage() {
// NOTE: This intentionally isn't proxied, to avoid call cycles.
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Build a new empty storage object for storing string indexes. Normally,
* this should be a concrete subclass of
* @{class:PhabricatorCustomFieldStringIndexStorage}.
*
* @return PhabricatorCustomFieldStringIndexStorage Storage object.
* @task appsearch
*/
protected function newNumericIndexStorage() {
// NOTE: This intentionally isn't proxied, to avoid call cycles.
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Build and populate storage for a string index.
*
* @param string String to index.
* @return PhabricatorCustomFieldStringIndexStorage Populated storage.
* @task appsearch
*/
protected function newStringIndex($value) {
if ($this->proxy) {
return $this->proxy->newStringIndex();
}
$key = $this->getFieldIndex();
return $this->newStringIndexStorage()
->setIndexKey($key)
->setIndexValue($value);
}
/**
* Build and populate storage for a numeric index.
*
* @param string Numeric value to index.
* @return PhabricatorCustomFieldNumericIndexStorage Populated storage.
* @task appsearch
*/
protected function newNumericIndex($value) {
if ($this->proxy) {
return $this->proxy->newNumericIndex();
}
$key = $this->getFieldIndex();
return $this->newNumericIndexStorage()
->setIndexKey($key)
->setIndexValue($value);
}
/**
* Read a query value from a request, for storage in a saved query. Normally,
* this method should, e.g., read a string out of the request.
*
* @param PhabricatorApplicationSearchEngine Engine building the query.
* @param AphrontRequest Request to read from.
* @return wild
* @task appsearch
*/
public function readApplicationSearchValueFromRequest(
PhabricatorApplicationSearchEngine $engine,
AphrontRequest $request) {
if ($this->proxy) {
return $this->proxy->readApplicationSearchValueFromRequest(
$engine,
$request);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Constrain a query, given a field value. Generally, this method should
* use `with...()` methods to apply filters or other constraints to the
* query.
*
* @param PhabricatorApplicationSearchEngine Engine executing the query.
* @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain.
* @param wild Constraint provided by the user.
* @return void
* @task appsearch
*/
public function applyApplicationSearchConstraintToQuery(
PhabricatorApplicationSearchEngine $engine,
PhabricatorCursorPagedPolicyAwareQuery $query,
$value) {
if ($this->proxy) {
return $this->proxy->applyApplicationSearchConstraintToQuery(
$engine,
$query,
$value);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Append search controls to the interface.
*
* @param PhabricatorApplicationSearchEngine Engine constructing the form.
* @param AphrontFormView The form to update.
* @param wild Value from the saved query.
* @return void
* @task appsearch
*/
public function appendToApplicationSearchForm(
PhabricatorApplicationSearchEngine $engine,
AphrontFormView $form,
$value) {
if ($this->proxy) {
return $this->proxy->appendToApplicationSearchForm(
$engine,
$form,
$value);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/* -( ApplicationTransactions )-------------------------------------------- */
/**
* Appearing in ApplicationTrasactions allows a field to be edited using
* standard workflows.
*
* @return bool True to appear in ApplicationTransactions.
* @task appxaction
*/
public function shouldAppearInApplicationTransactions() {
if ($this->proxy) {
return $this->proxy->shouldAppearInApplicationTransactions();
}
return false;
}
/**
* @task appxaction
*/
public function getApplicationTransactionType() {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionType();
}
return PhabricatorTransactions::TYPE_CUSTOMFIELD;
}
/**
* @task appxaction
*/
public function getApplicationTransactionMetadata() {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionMetadata();
}
return array();
}
/**
* @task appxaction
*/
public function getOldValueForApplicationTransactions() {
if ($this->proxy) {
return $this->proxy->getOldValueForApplicationTransactions();
}
return $this->getValueForStorage();
}
/**
* @task appxaction
*/
public function getNewValueForApplicationTransactions() {
if ($this->proxy) {
return $this->proxy->getNewValueForApplicationTransactions();
}
return $this->getValueForStorage();
}
/**
* @task appxaction
*/
public function setValueFromApplicationTransactions($value) {
if ($this->proxy) {
return $this->proxy->setValueFromApplicationTransactions($value);
}
return $this->setValueFromStorage($value);
}
/**
* @task appxaction
*/
public function getNewValueFromApplicationTransactions(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getNewValueFromApplicationTransactions($xaction);
}
return $xaction->getNewValue();
}
/**
* @task appxaction
*/
public function getApplicationTransactionHasEffect(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionHasEffect($xaction);
}
return ($xaction->getOldValue() !== $xaction->getNewValue());
}
/**
* @task appxaction
*/
public function applyApplicationTransactionInternalEffects(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->applyApplicationTransactionInternalEffects($xaction);
}
return;
}
/**
* @task appxaction
*/
public function getApplicationTransactionRemarkupBlocks(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionRemarkupBlocks($xaction);
}
return array();
}
/**
* @task appxaction
*/
public function applyApplicationTransactionExternalEffects(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->applyApplicationTransactionExternalEffects($xaction);
}
if (!$this->shouldEnableForRole(self::ROLE_STORAGE)) {
return;
}
$this->setValueFromApplicationTransactions($xaction->getNewValue());
$value = $this->getValueForStorage();
$table = $this->newStorageObject();
$conn_w = $table->establishConnection('w');
if ($value === null) {
queryfx(
$conn_w,
'DELETE FROM %T WHERE objectPHID = %s AND fieldIndex = %s',
$table->getTableName(),
$this->getObject()->getPHID(),
$this->getFieldIndex());
} else {
queryfx(
$conn_w,
'INSERT INTO %T (objectPHID, fieldIndex, fieldValue)
VALUES (%s, %s, %s)
ON DUPLICATE KEY UPDATE fieldValue = VALUES(fieldValue)',
$table->getTableName(),
$this->getObject()->getPHID(),
$this->getFieldIndex(),
$value);
}
return;
}
/**
* Validate transactions for an object. This allows you to raise an error
* when a transaction would set a field to an invalid value, or when a field
* is required but no transactions provide value.
*
* @param PhabricatorLiskDAO Editor applying the transactions.
* @param string Transaction type. This type is always
* `PhabricatorTransactions::TYPE_CUSTOMFIELD`, it is provided for
* convenience when constructing exceptions.
* @param list<PhabricatorApplicationTransaction> Transactions being applied,
* which may be empty if this field is not being edited.
* @return list<PhabricatorApplicationTransactionValidationError> Validation
* errors.
*
* @task appxaction
*/
public function validateApplicationTransactions(
PhabricatorApplicationTransactionEditor $editor,
$type,
array $xactions) {
if ($this->proxy) {
return $this->proxy->validateApplicationTransactions(
$editor,
$type,
$xactions);
}
return array();
}
public function getApplicationTransactionTitle(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionTitle(
$xaction);
}
$author_phid = $xaction->getAuthorPHID();
return pht(
'%s updated this object.',
$xaction->renderHandleLink($author_phid));
}
public function getApplicationTransactionTitleForFeed(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionTitleForFeed(
$xaction);
}
$author_phid = $xaction->getAuthorPHID();
$object_phid = $xaction->getObjectPHID();
return pht(
'%s updated %s.',
$xaction->renderHandleLink($author_phid),
$xaction->renderHandleLink($object_phid));
}
public function getApplicationTransactionHasChangeDetails(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionHasChangeDetails(
$xaction);
}
return false;
}
public function getApplicationTransactionChangeDetails(
PhabricatorApplicationTransaction $xaction,
PhabricatorUser $viewer) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionChangeDetails(
$xaction,
$viewer);
}
return null;
}
public function getApplicationTransactionRequiredHandlePHIDs(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->getApplicationTransactionRequiredHandlePHIDs(
$xaction);
}
return array();
}
public function shouldHideInApplicationTransactions(
PhabricatorApplicationTransaction $xaction) {
if ($this->proxy) {
return $this->proxy->shouldHideInApplicationTransactions($xaction);
}
return false;
}
/* -( Transaction Mail )--------------------------------------------------- */
/**
* @task xactionmail
*/
public function shouldAppearInTransactionMail() {
if ($this->proxy) {
return $this->proxy->shouldAppearInTransactionMail();
}
return false;
}
/**
* @task xactionmail
*/
public function updateTransactionMailBody(
PhabricatorMetaMTAMailBody $body,
PhabricatorApplicationTransactionEditor $editor,
array $xactions) {
if ($this->proxy) {
return $this->proxy->updateTransactionMailBody($body, $editor, $xactions);
}
return;
}
/* -( Edit View )---------------------------------------------------------- */
public function getEditEngineFields(PhabricatorEditEngine $engine) {
$field = $this->newStandardEditField();
return array(
$field,
);
}
protected function newEditField() {
$field = id(new PhabricatorCustomFieldEditField())
->setCustomField($this);
$http_type = $this->getHTTPParameterType();
if ($http_type) {
$field->setCustomFieldHTTPParameterType($http_type);
}
$conduit_type = $this->getConduitEditParameterType();
if ($conduit_type) {
$field->setCustomFieldConduitParameterType($conduit_type);
}
$bulk_type = $this->getBulkParameterType();
if ($bulk_type) {
$field->setCustomFieldBulkParameterType($bulk_type);
}
$comment_action = $this->getCommentAction();
if ($comment_action) {
$field
->setCustomFieldCommentAction($comment_action)
->setCommentActionLabel(
pht(
'Change %s',
$this->getFieldName()));
}
return $field;
}
protected function newStandardEditField() {
if ($this->proxy) {
return $this->proxy->newStandardEditField();
}
if ($this->shouldAppearInEditView()) {
$form_field = true;
} else {
$form_field = false;
}
$bulk_label = $this->getBulkEditLabel();
return $this->newEditField()
->setKey($this->getFieldKey())
->setEditTypeKey($this->getModernFieldKey())
->setLabel($this->getFieldName())
->setBulkEditLabel($bulk_label)
->setDescription($this->getFieldDescription())
->setTransactionType($this->getApplicationTransactionType())
->setIsFormField($form_field)
->setValue($this->getNewValueForApplicationTransactions());
}
protected function getBulkEditLabel() {
if ($this->proxy) {
return $this->proxy->getBulkEditLabel();
}
return pht('Set "%s" to', $this->getFieldName());
}
public function getBulkParameterType() {
return $this->newBulkParameterType();
}
protected function newBulkParameterType() {
if ($this->proxy) {
return $this->proxy->newBulkParameterType();
}
return null;
}
protected function getHTTPParameterType() {
if ($this->proxy) {
return $this->proxy->getHTTPParameterType();
}
return null;
}
/**
* @task edit
*/
public function shouldAppearInEditView() {
if ($this->proxy) {
return $this->proxy->shouldAppearInEditView();
}
return false;
}
/**
* @task edit
*/
public function shouldAppearInEditEngine() {
if ($this->proxy) {
return $this->proxy->shouldAppearInEditEngine();
}
return false;
}
/**
* @task edit
*/
public function readValueFromRequest(AphrontRequest $request) {
if ($this->proxy) {
return $this->proxy->readValueFromRequest($request);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* @task edit
*/
public function getRequiredHandlePHIDsForEdit() {
if ($this->proxy) {
return $this->proxy->getRequiredHandlePHIDsForEdit();
}
return array();
}
/**
* @task edit
*/
public function getInstructionsForEdit() {
if ($this->proxy) {
return $this->proxy->getInstructionsForEdit();
}
return null;
}
/**
* @task edit
*/
public function renderEditControl(array $handles) {
if ($this->proxy) {
return $this->proxy->renderEditControl($handles);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/* -( Property View )------------------------------------------------------ */
/**
* @task view
*/
public function shouldAppearInPropertyView() {
if ($this->proxy) {
return $this->proxy->shouldAppearInPropertyView();
}
return false;
}
/**
* @task view
*/
public function renderPropertyViewLabel() {
if ($this->proxy) {
return $this->proxy->renderPropertyViewLabel();
}
return $this->getFieldName();
}
/**
* @task view
*/
public function renderPropertyViewValue(array $handles) {
if ($this->proxy) {
return $this->proxy->renderPropertyViewValue($handles);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* @task view
*/
public function getStyleForPropertyView() {
if ($this->proxy) {
return $this->proxy->getStyleForPropertyView();
}
return 'property';
}
/**
* @task view
*/
public function getIconForPropertyView() {
if ($this->proxy) {
return $this->proxy->getIconForPropertyView();
}
return null;
}
/**
* @task view
*/
public function getRequiredHandlePHIDsForPropertyView() {
if ($this->proxy) {
return $this->proxy->getRequiredHandlePHIDsForPropertyView();
}
return array();
}
/* -( List View )---------------------------------------------------------- */
/**
* @task list
*/
public function shouldAppearInListView() {
if ($this->proxy) {
return $this->proxy->shouldAppearInListView();
}
return false;
}
/**
* @task list
*/
public function renderOnListItem(PHUIObjectItemView $view) {
if ($this->proxy) {
return $this->proxy->renderOnListItem($view);
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/* -( Global Search )------------------------------------------------------ */
/**
* @task globalsearch
*/
public function shouldAppearInGlobalSearch() {
if ($this->proxy) {
return $this->proxy->shouldAppearInGlobalSearch();
}
return false;
}
/**
* @task globalsearch
*/
public function updateAbstractDocument(
PhabricatorSearchAbstractDocument $document) {
if ($this->proxy) {
return $this->proxy->updateAbstractDocument($document);
}
return $document;
}
/* -( Data Export )-------------------------------------------------------- */
public function shouldAppearInDataExport() {
if ($this->proxy) {
return $this->proxy->shouldAppearInDataExport();
}
try {
$this->newExportFieldType();
return true;
} catch (PhabricatorCustomFieldImplementationIncompleteException $ex) {
return false;
}
}
public function newExportField() {
if ($this->proxy) {
return $this->proxy->newExportField();
}
return $this->newExportFieldType()
->setLabel($this->getFieldName());
}
public function newExportData() {
if ($this->proxy) {
return $this->proxy->newExportData();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
protected function newExportFieldType() {
if ($this->proxy) {
return $this->proxy->newExportFieldType();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/* -( Conduit )------------------------------------------------------------ */
/**
* @task conduit
*/
public function shouldAppearInConduitDictionary() {
if ($this->proxy) {
return $this->proxy->shouldAppearInConduitDictionary();
}
return false;
}
/**
* @task conduit
*/
public function getConduitDictionaryValue() {
if ($this->proxy) {
return $this->proxy->getConduitDictionaryValue();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
public function shouldAppearInConduitTransactions() {
if ($this->proxy) {
return $this->proxy->shouldAppearInConduitDictionary();
}
return false;
}
public function getConduitSearchParameterType() {
return $this->newConduitSearchParameterType();
}
protected function newConduitSearchParameterType() {
if ($this->proxy) {
return $this->proxy->newConduitSearchParameterType();
}
return null;
}
public function getConduitEditParameterType() {
return $this->newConduitEditParameterType();
}
protected function newConduitEditParameterType() {
if ($this->proxy) {
return $this->proxy->newConduitEditParameterType();
}
return null;
}
public function getCommentAction() {
return $this->newCommentAction();
}
protected function newCommentAction() {
if ($this->proxy) {
return $this->proxy->newCommentAction();
}
return null;
}
/* -( Herald )------------------------------------------------------------- */
/**
* Return `true` to make this field available in Herald.
*
* @return bool True to expose the field in Herald.
* @task herald
*/
public function shouldAppearInHerald() {
if ($this->proxy) {
return $this->proxy->shouldAppearInHerald();
}
return false;
}
/**
* Get the name of the field in Herald. By default, this uses the
* normal field name.
*
* @return string Herald field name.
* @task herald
*/
public function getHeraldFieldName() {
if ($this->proxy) {
return $this->proxy->getHeraldFieldName();
}
return $this->getFieldName();
}
/**
* Get the field value for evaluation by Herald.
*
* @return wild Field value.
* @task herald
*/
public function getHeraldFieldValue() {
if ($this->proxy) {
return $this->proxy->getHeraldFieldValue();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Get the available conditions for this field in Herald.
*
* @return list<const> List of Herald condition constants.
* @task herald
*/
public function getHeraldFieldConditions() {
if ($this->proxy) {
return $this->proxy->getHeraldFieldConditions();
}
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
/**
* Get the Herald value type for the given condition.
*
* @param const Herald condition constant.
* @return const|null Herald value type, or null to use the default.
* @task herald
*/
public function getHeraldFieldValueType($condition) {
if ($this->proxy) {
return $this->proxy->getHeraldFieldValueType($condition);
}
return null;
}
public function getHeraldFieldStandardType() {
if ($this->proxy) {
return $this->proxy->getHeraldFieldStandardType();
}
return null;
}
public function getHeraldDatasource() {
if ($this->proxy) {
return $this->proxy->getHeraldDatasource();
}
return null;
}
public function shouldAppearInHeraldActions() {
if ($this->proxy) {
return $this->proxy->shouldAppearInHeraldActions();
}
return false;
}
public function getHeraldActionName() {
if ($this->proxy) {
return $this->proxy->getHeraldActionName();
}
return null;
}
public function getHeraldActionStandardType() {
if ($this->proxy) {
return $this->proxy->getHeraldActionStandardType();
}
return null;
}
public function getHeraldActionDescription($value) {
if ($this->proxy) {
return $this->proxy->getHeraldActionDescription($value);
}
return null;
}
public function getHeraldActionEffectDescription($value) {
if ($this->proxy) {
return $this->proxy->getHeraldActionEffectDescription($value);
}
return null;
}
public function getHeraldActionDatasource() {
if ($this->proxy) {
return $this->proxy->getHeraldActionDatasource();
}
return null;
}
+ private static function adjustCustomFieldsForObjectSubtype(
+ PhabricatorCustomFieldInterface $object,
+ $role,
+ array $fields) {
+ assert_instances_of($fields, __CLASS__);
+
+ // We only apply subtype adjustment for some roles. For example, when
+ // writing Herald rules or building a Search interface, we always want to
+ // show all the fields in their default state, so we do not apply any
+ // adjustments.
+ $subtype_roles = array(
+ self::ROLE_EDITENGINE,
+ self::ROLE_VIEW,
+ );
+
+ $subtype_roles = array_fuse($subtype_roles);
+ if (!isset($subtype_roles[$role])) {
+ return $fields;
+ }
+
+ // If the object doesn't support subtypes, we can't possibly make
+ // any adjustments based on subtype.
+ if (!($object instanceof PhabricatorEditEngineSubtypeInterface)) {
+ return $fields;
+ }
+
+ $subtype_map = $object->newEditEngineSubtypeMap();
+ $subtype_key = $object->getEditEngineSubtype();
+ $subtype_object = $subtype_map->getSubtype($subtype_key);
+
+ $map = array();
+ foreach ($fields as $field) {
+ $modern_key = $field->getModernFieldKey();
+ if (!strlen($modern_key)) {
+ continue;
+ }
+
+ $map[$modern_key] = $field;
+ }
+
+ foreach ($map as $field_key => $field) {
+ // For now, only support overriding standard custom fields. In the
+ // future there's no technical or product reason we couldn't let you
+ // override (some properites of) other fields like "Title", but they
+ // don't usually support appropriate "setX()" methods today.
+ if (!($field instanceof PhabricatorStandardCustomField)) {
+ // For fields that are proxies on top of StandardCustomField, which
+ // is how most application custom fields work today, we can reconfigure
+ // the proxied field instead.
+ $field = $field->getProxy();
+ if (!$field || !($field instanceof PhabricatorStandardCustomField)) {
+ continue;
+ }
+ }
+
+ $subtype_config = $subtype_object->getSubtypeFieldConfiguration(
+ $field_key);
+
+ if (!$subtype_config) {
+ continue;
+ }
+
+ if (isset($subtype_config['disabled'])) {
+ $field->setIsEnabled(!$subtype_config['disabled']);
+ }
+
+ if (isset($subtype_config['name'])) {
+ $field->setFieldName($subtype_config['name']);
+ }
+ }
+
+ return $fields;
+ }
+
}
diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php
index 5bd6256b7..4c0bce861 100644
--- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php
+++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php
@@ -1,503 +1,517 @@
<?php
abstract class PhabricatorStandardCustomField
extends PhabricatorCustomField {
private $rawKey;
private $fieldKey;
private $fieldName;
private $fieldValue;
private $fieldDescription;
private $fieldConfig;
private $applicationField;
private $strings = array();
private $caption;
private $fieldError;
private $required;
private $default;
private $isCopyable;
private $hasStorageValue;
private $isBuiltin;
+ private $isEnabled = true;
abstract public function getFieldType();
public static function buildStandardFields(
PhabricatorCustomField $template,
array $config,
$builtin = false) {
$types = id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getFieldType')
->execute();
$fields = array();
foreach ($config as $key => $value) {
$type = idx($value, 'type', 'text');
if (empty($types[$type])) {
// TODO: We should have better typechecking somewhere, and then make
// this more serious.
continue;
}
$namespace = $template->getStandardCustomFieldNamespace();
$full_key = "std:{$namespace}:{$key}";
$template = clone $template;
$standard = id(clone $types[$type])
->setRawStandardFieldKey($key)
->setFieldKey($full_key)
->setFieldConfig($value)
->setApplicationField($template);
if ($builtin) {
$standard->setIsBuiltin(true);
}
$field = $template->setProxy($standard);
$fields[] = $field;
}
return $fields;
}
public function setApplicationField(
PhabricatorStandardCustomFieldInterface $application_field) {
$this->applicationField = $application_field;
return $this;
}
public function getApplicationField() {
return $this->applicationField;
}
public function setFieldName($name) {
$this->fieldName = $name;
return $this;
}
public function getFieldValue() {
return $this->fieldValue;
}
public function setFieldValue($value) {
$this->fieldValue = $value;
return $this;
}
public function setCaption($caption) {
$this->caption = $caption;
return $this;
}
public function getCaption() {
return $this->caption;
}
public function setFieldDescription($description) {
$this->fieldDescription = $description;
return $this;
}
public function setIsBuiltin($is_builtin) {
$this->isBuiltin = $is_builtin;
return $this;
}
public function getIsBuiltin() {
return $this->isBuiltin;
}
public function setFieldConfig(array $config) {
foreach ($config as $key => $value) {
switch ($key) {
case 'name':
$this->setFieldName($value);
break;
case 'description':
$this->setFieldDescription($value);
break;
case 'strings':
$this->setStrings($value);
break;
case 'caption':
$this->setCaption($value);
break;
case 'required':
if ($value) {
$this->setRequired($value);
$this->setFieldError(true);
}
break;
case 'default':
$this->setFieldValue($value);
break;
case 'copy':
$this->setIsCopyable($value);
break;
case 'type':
// We set this earlier on.
break;
}
}
$this->fieldConfig = $config;
return $this;
}
public function getFieldConfigValue($key, $default = null) {
return idx($this->fieldConfig, $key, $default);
}
public function setFieldError($field_error) {
$this->fieldError = $field_error;
return $this;
}
public function getFieldError() {
return $this->fieldError;
}
public function setRequired($required) {
$this->required = $required;
return $this;
}
public function getRequired() {
return $this->required;
}
public function setRawStandardFieldKey($raw_key) {
$this->rawKey = $raw_key;
return $this;
}
public function getRawStandardFieldKey() {
return $this->rawKey;
}
+ public function setIsEnabled($is_enabled) {
+ $this->isEnabled = $is_enabled;
+ return $this;
+ }
+
+ public function getIsEnabled() {
+ return $this->isEnabled;
+ }
+
+ public function isFieldEnabled() {
+ return $this->getIsEnabled();
+ }
+
/* -( PhabricatorCustomField )--------------------------------------------- */
public function setFieldKey($field_key) {
$this->fieldKey = $field_key;
return $this;
}
public function getFieldKey() {
return $this->fieldKey;
}
public function getFieldName() {
return coalesce($this->fieldName, parent::getFieldName());
}
public function getFieldDescription() {
return coalesce($this->fieldDescription, parent::getFieldDescription());
}
public function setStrings(array $strings) {
$this->strings = $strings;
return;
}
public function getString($key, $default = null) {
return idx($this->strings, $key, $default);
}
public function setIsCopyable($is_copyable) {
$this->isCopyable = $is_copyable;
return $this;
}
public function getIsCopyable() {
return $this->isCopyable;
}
public function shouldUseStorage() {
try {
$object = $this->newStorageObject();
return true;
} catch (PhabricatorCustomFieldImplementationIncompleteException $ex) {
return false;
}
}
public function getValueForStorage() {
return $this->getFieldValue();
}
public function setValueFromStorage($value) {
return $this->setFieldValue($value);
}
public function didSetValueFromStorage() {
$this->hasStorageValue = true;
return $this;
}
public function getOldValueForApplicationTransactions() {
if ($this->hasStorageValue) {
return $this->getValueForStorage();
} else {
return null;
}
}
public function shouldAppearInApplicationTransactions() {
return true;
}
public function shouldAppearInEditView() {
return $this->getFieldConfigValue('edit', true);
}
public function readValueFromRequest(AphrontRequest $request) {
$value = $request->getStr($this->getFieldKey());
if (!strlen($value)) {
$value = null;
}
$this->setFieldValue($value);
}
public function getInstructionsForEdit() {
return $this->getFieldConfigValue('instructions');
}
public function getPlaceholder() {
return $this->getFieldConfigValue('placeholder', null);
}
public function renderEditControl(array $handles) {
return id(new AphrontFormTextControl())
->setName($this->getFieldKey())
->setCaption($this->getCaption())
->setValue($this->getFieldValue())
->setError($this->getFieldError())
->setLabel($this->getFieldName())
->setPlaceholder($this->getPlaceholder());
}
public function newStorageObject() {
return $this->getApplicationField()->newStorageObject();
}
public function shouldAppearInPropertyView() {
return $this->getFieldConfigValue('view', true);
}
public function renderPropertyViewValue(array $handles) {
if (!strlen($this->getFieldValue())) {
return null;
}
return $this->getFieldValue();
}
public function shouldAppearInApplicationSearch() {
return $this->getFieldConfigValue('search', false);
}
protected function newStringIndexStorage() {
return $this->getApplicationField()->newStringIndexStorage();
}
protected function newNumericIndexStorage() {
return $this->getApplicationField()->newNumericIndexStorage();
}
public function buildFieldIndexes() {
return array();
}
public function buildOrderIndex() {
return null;
}
public function readApplicationSearchValueFromRequest(
PhabricatorApplicationSearchEngine $engine,
AphrontRequest $request) {
return;
}
public function applyApplicationSearchConstraintToQuery(
PhabricatorApplicationSearchEngine $engine,
PhabricatorCursorPagedPolicyAwareQuery $query,
$value) {
return;
}
public function appendToApplicationSearchForm(
PhabricatorApplicationSearchEngine $engine,
AphrontFormView $form,
$value) {
return;
}
public function validateApplicationTransactions(
PhabricatorApplicationTransactionEditor $editor,
$type,
array $xactions) {
$this->setFieldError(null);
$errors = parent::validateApplicationTransactions(
$editor,
$type,
$xactions);
if ($this->getRequired()) {
$value = $this->getOldValueForApplicationTransactions();
$transaction = null;
foreach ($xactions as $xaction) {
$value = $xaction->getNewValue();
if (!$this->isValueEmpty($value)) {
$transaction = $xaction;
break;
}
}
if ($this->isValueEmpty($value)) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht('%s is required.', $this->getFieldName()),
$transaction);
$error->setIsMissingFieldError(true);
$errors[] = $error;
$this->setFieldError(pht('Required'));
}
}
return $errors;
}
protected function isValueEmpty($value) {
if (is_array($value)) {
return empty($value);
}
return !strlen($value);
}
public function getApplicationTransactionTitle(
PhabricatorApplicationTransaction $xaction) {
$author_phid = $xaction->getAuthorPHID();
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
if (!$old) {
return pht(
'%s set %s to %s.',
$xaction->renderHandleLink($author_phid),
$this->getFieldName(),
$new);
} else if (!$new) {
return pht(
'%s removed %s.',
$xaction->renderHandleLink($author_phid),
$this->getFieldName());
} else {
return pht(
'%s changed %s from %s to %s.',
$xaction->renderHandleLink($author_phid),
$this->getFieldName(),
$old,
$new);
}
}
public function getApplicationTransactionTitleForFeed(
PhabricatorApplicationTransaction $xaction) {
$author_phid = $xaction->getAuthorPHID();
$object_phid = $xaction->getObjectPHID();
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
if (!$old) {
return pht(
'%s set %s to %s on %s.',
$xaction->renderHandleLink($author_phid),
$this->getFieldName(),
$new,
$xaction->renderHandleLink($object_phid));
} else if (!$new) {
return pht(
'%s removed %s on %s.',
$xaction->renderHandleLink($author_phid),
$this->getFieldName(),
$xaction->renderHandleLink($object_phid));
} else {
return pht(
'%s changed %s from %s to %s on %s.',
$xaction->renderHandleLink($author_phid),
$this->getFieldName(),
$old,
$new,
$xaction->renderHandleLink($object_phid));
}
}
public function getHeraldFieldValue() {
return $this->getFieldValue();
}
public function getFieldControlID($key = null) {
$key = coalesce($key, $this->getRawStandardFieldKey());
return 'std:control:'.$key;
}
public function shouldAppearInGlobalSearch() {
return $this->getFieldConfigValue('fulltext', false);
}
public function updateAbstractDocument(
PhabricatorSearchAbstractDocument $document) {
$field_key = $this->getFieldConfigValue('fulltext');
// If the caller or configuration didn't specify a valid field key,
// generate one automatically from the field index.
if (!is_string($field_key) || (strlen($field_key) != 4)) {
$field_key = '!'.substr($this->getFieldIndex(), 0, 3);
}
$field_value = $this->getFieldValue();
if (strlen($field_value)) {
$document->addField($field_key, $field_value);
}
}
protected function newStandardEditField() {
$short = $this->getModernFieldKey();
return parent::newStandardEditField()
->setEditTypeKey($short)
->setIsCopyable($this->getIsCopyable());
}
public function shouldAppearInConduitTransactions() {
return true;
}
public function shouldAppearInConduitDictionary() {
return true;
}
public function getModernFieldKey() {
if ($this->getIsBuiltin()) {
return $this->getRawStandardFieldKey();
} else {
return 'custom.'.$this->getRawStandardFieldKey();
}
}
public function getConduitDictionaryValue() {
return $this->getFieldValue();
}
public function newExportData() {
return $this->getFieldValue();
}
}
diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php
index 036b7301a..5957afe56 100644
--- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php
+++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php
@@ -1,156 +1,161 @@
<?php
final class PhabricatorStandardCustomFieldSelect
extends PhabricatorStandardCustomField {
public function getFieldType() {
return 'select';
}
public function buildFieldIndexes() {
$indexes = array();
$value = $this->getFieldValue();
if (strlen($value)) {
$indexes[] = $this->newStringIndex($value);
}
return $indexes;
}
public function readApplicationSearchValueFromRequest(
PhabricatorApplicationSearchEngine $engine,
AphrontRequest $request) {
return $request->getArr($this->getFieldKey());
}
public function applyApplicationSearchConstraintToQuery(
PhabricatorApplicationSearchEngine $engine,
PhabricatorCursorPagedPolicyAwareQuery $query,
$value) {
if ($value) {
$query->withApplicationSearchContainsConstraint(
$this->newStringIndex(null),
$value);
}
}
public function appendToApplicationSearchForm(
PhabricatorApplicationSearchEngine $engine,
AphrontFormView $form,
$value) {
if (!is_array($value)) {
$value = array();
}
$value = array_fuse($value);
$control = id(new AphrontFormCheckboxControl())
->setLabel($this->getFieldName());
foreach ($this->getOptions() as $name => $option) {
$control->addCheckbox(
$this->getFieldKey().'[]',
$name,
$option,
isset($value[$name]));
}
$form->appendChild($control);
}
public function getOptions() {
return $this->getFieldConfigValue('options', array());
}
public function renderEditControl(array $handles) {
return id(new AphrontFormSelectControl())
->setLabel($this->getFieldName())
->setCaption($this->getCaption())
->setName($this->getFieldKey())
->setValue($this->getFieldValue())
->setOptions($this->getOptions());
}
public function renderPropertyViewValue(array $handles) {
if (!strlen($this->getFieldValue())) {
return null;
}
return idx($this->getOptions(), $this->getFieldValue());
}
public function getApplicationTransactionTitle(
PhabricatorApplicationTransaction $xaction) {
$author_phid = $xaction->getAuthorPHID();
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$old = idx($this->getOptions(), $old, $old);
$new = idx($this->getOptions(), $new, $new);
if (!$old) {
return pht(
'%s set %s to %s.',
$xaction->renderHandleLink($author_phid),
$this->getFieldName(),
$new);
} else if (!$new) {
return pht(
'%s removed %s.',
$xaction->renderHandleLink($author_phid),
$this->getFieldName());
} else {
return pht(
'%s changed %s from %s to %s.',
$xaction->renderHandleLink($author_phid),
$this->getFieldName(),
$old,
$new);
}
}
public function shouldAppearInHerald() {
return true;
}
public function getHeraldFieldConditions() {
return array(
HeraldAdapter::CONDITION_IS_ANY,
HeraldAdapter::CONDITION_IS_NOT_ANY,
);
}
public function getHeraldFieldValueType($condition) {
$parameters = array(
'object' => get_class($this->getObject()),
'role' => PhabricatorCustomField::ROLE_HERALD,
'key' => $this->getFieldKey(),
);
$datasource = id(new PhabricatorStandardSelectCustomFieldDatasource())
->setParameters($parameters);
return id(new HeraldTokenizerFieldValue())
->setKey('custom.'.$this->getFieldKey())
->setDatasource($datasource)
->setValueMap($this->getOptions());
}
protected function getHTTPParameterType() {
return new AphrontSelectHTTPParameterType();
}
protected function newConduitSearchParameterType() {
return new ConduitStringListParameterType();
}
protected function newConduitEditParameterType() {
return new ConduitStringParameterType();
}
protected function newBulkParameterType() {
return id(new BulkSelectParameterType())
->setOptions($this->getOptions());
}
+ protected function newExportFieldType() {
+ return id(new PhabricatorOptionExportField())
+ ->setOptions($this->getOptions());
+ }
+
}
diff --git a/src/infrastructure/daemon/workers/PhabricatorWorker.php b/src/infrastructure/daemon/workers/PhabricatorWorker.php
index 1b9821b68..f055544b7 100644
--- a/src/infrastructure/daemon/workers/PhabricatorWorker.php
+++ b/src/infrastructure/daemon/workers/PhabricatorWorker.php
@@ -1,314 +1,315 @@
<?php
/**
* @task config Configuring Retries and Failures
*/
abstract class PhabricatorWorker extends Phobject {
private $data;
private static $runAllTasksInProcess = false;
private $queuedTasks = array();
private $currentWorkerTask;
// NOTE: Lower priority numbers execute first. The priority numbers have to
// have the same ordering that IDs do (lowest first) so MySQL can use a
// multipart key across both of them efficiently.
const PRIORITY_ALERTS = 1000;
const PRIORITY_DEFAULT = 2000;
const PRIORITY_COMMIT = 2500;
const PRIORITY_BULK = 3000;
+ const PRIORITY_INDEX = 3500;
const PRIORITY_IMPORT = 4000;
/**
* Special owner indicating that the task has yielded.
*/
const YIELD_OWNER = '(yield)';
/* -( Configuring Retries and Failures )----------------------------------- */
/**
* Return the number of seconds this worker needs hold a lease on the task for
* while it performs work. For most tasks you can leave this at `null`, which
* will give you a default lease (currently 2 hours).
*
* For tasks which may take a very long time to complete, you should return
* an upper bound on the amount of time the task may require.
*
* @return int|null Number of seconds this task needs to remain leased for,
* or null for a default lease.
*
* @task config
*/
public function getRequiredLeaseTime() {
return null;
}
/**
* Return the maximum number of times this task may be retried before it is
* considered permanently failed. By default, tasks retry indefinitely. You
* can throw a @{class:PhabricatorWorkerPermanentFailureException} to cause an
* immediate permanent failure.
*
* @return int|null Number of times the task will retry before permanent
* failure. Return `null` to retry indefinitely.
*
* @task config
*/
public function getMaximumRetryCount() {
return null;
}
/**
* Return the number of seconds a task should wait after a failure before
* retrying. For most tasks you can leave this at `null`, which will give you
* a short default retry period (currently 60 seconds).
*
* @param PhabricatorWorkerTask The task itself. This object is probably
* useful mostly to examine the failure count
* if you want to implement staggered retries,
* or to examine the execution exception if
* you want to react to different failures in
* different ways.
* @return int|null Number of seconds to wait between retries,
* or null for a default retry period
* (currently 60 seconds).
*
* @task config
*/
public function getWaitBeforeRetry(PhabricatorWorkerTask $task) {
return null;
}
public function setCurrentWorkerTask(PhabricatorWorkerTask $task) {
$this->currentWorkerTask = $task;
return $this;
}
public function getCurrentWorkerTask() {
return $this->currentWorkerTask;
}
public function getCurrentWorkerTaskID() {
$task = $this->getCurrentWorkerTask();
if (!$task) {
return null;
}
return $task->getID();
}
abstract protected function doWork();
final public function __construct($data) {
$this->data = $data;
}
final protected function getTaskData() {
return $this->data;
}
final protected function getTaskDataValue($key, $default = null) {
$data = $this->getTaskData();
if (!is_array($data)) {
throw new PhabricatorWorkerPermanentFailureException(
pht('Expected task data to be a dictionary.'));
}
return idx($data, $key, $default);
}
final public function executeTask() {
$this->doWork();
}
final public static function scheduleTask(
$task_class,
$data,
$options = array()) {
PhutilTypeSpec::checkMap(
$options,
array(
'priority' => 'optional int|null',
'objectPHID' => 'optional string|null',
'delayUntil' => 'optional int|null',
));
$priority = idx($options, 'priority');
if ($priority === null) {
$priority = self::PRIORITY_DEFAULT;
}
$object_phid = idx($options, 'objectPHID');
$task = id(new PhabricatorWorkerActiveTask())
->setTaskClass($task_class)
->setData($data)
->setPriority($priority)
->setObjectPHID($object_phid);
$delay = idx($options, 'delayUntil');
if ($delay) {
$task->setLeaseExpires($delay);
}
if (self::$runAllTasksInProcess) {
// Do the work in-process.
$worker = newv($task_class, array($data));
while (true) {
try {
$worker->executeTask();
$worker->flushTaskQueue();
break;
} catch (PhabricatorWorkerYieldException $ex) {
phlog(
pht(
'In-process task "%s" yielded for %s seconds, sleeping...',
$task_class,
$ex->getDuration()));
sleep($ex->getDuration());
}
}
// Now, save a task row and immediately archive it so we can return an
// object with a valid ID.
$task->openTransaction();
$task->save();
$archived = $task->archiveTask(
PhabricatorWorkerArchiveTask::RESULT_SUCCESS,
0);
$task->saveTransaction();
return $archived;
} else {
$task->save();
return $task;
}
}
public function renderForDisplay(PhabricatorUser $viewer) {
return null;
}
/**
* Set this flag to execute scheduled tasks synchronously, in the same
* process. This is useful for debugging, and otherwise dramatically worse
* in every way imaginable.
*/
public static function setRunAllTasksInProcess($all) {
self::$runAllTasksInProcess = $all;
}
final protected function log($pattern /* , ... */) {
$console = PhutilConsole::getConsole();
$argv = func_get_args();
call_user_func_array(array($console, 'writeLog'), $argv);
return $this;
}
/**
* Queue a task to be executed after this one succeeds.
*
* The followup task will be queued only if this task completes cleanly.
*
* @param string Task class to queue.
* @param array Data for the followup task.
* @param array Options for the followup task.
* @return this
*/
final protected function queueTask(
$class,
array $data,
array $options = array()) {
$this->queuedTasks[] = array($class, $data, $options);
return $this;
}
/**
* Get tasks queued as followups by @{method:queueTask}.
*
* @return list<tuple<string, wild, int|null>> Queued task specifications.
*/
final protected function getQueuedTasks() {
return $this->queuedTasks;
}
/**
* Schedule any queued tasks, then empty the task queue.
*
* By default, the queue is flushed only if a task succeeds. You can call
* this method to force the queue to flush before failing (for example, if
* you are using queues to improve locking behavior).
*
* @param map<string, wild> Optional default options.
* @return this
*/
final public function flushTaskQueue($defaults = array()) {
foreach ($this->getQueuedTasks() as $task) {
list($class, $data, $options) = $task;
$options = $options + $defaults;
self::scheduleTask($class, $data, $options);
}
$this->queuedTasks = array();
}
/**
* Awaken tasks that have yielded.
*
* Reschedules the specified tasks if they are currently queued in a yielded,
* unleased, unretried state so they'll execute sooner. This can let the
* queue avoid unnecessary waits.
*
* This method does not provide any assurances about when these tasks will
* execute, or even guarantee that it will have any effect at all.
*
* @param list<id> List of task IDs to try to awaken.
* @return void
*/
final public static function awakenTaskIDs(array $ids) {
if (!$ids) {
return;
}
$table = new PhabricatorWorkerActiveTask();
$conn_w = $table->establishConnection('w');
// NOTE: At least for now, we're keeping these tasks yielded, just
// pretending that they threw a shorter yield than they really did.
// Overlap the windows here to handle minor client/server time differences
// and because it's likely correct to push these tasks to the head of their
// respective priorities. There is a good chance they are ready to execute.
$window = phutil_units('1 hour in seconds');
$epoch_ago = (PhabricatorTime::getNow() - $window);
queryfx(
$conn_w,
'UPDATE %T SET leaseExpires = %d
WHERE id IN (%Ld)
AND leaseOwner = %s
AND leaseExpires > %d
AND failureCount = 0',
$table->getTableName(),
$epoch_ago,
$ids,
self::YIELD_OWNER,
$epoch_ago);
}
protected function newContentSource() {
return PhabricatorContentSource::newForSource(
PhabricatorDaemonContentSource::SOURCECONST);
}
}
diff --git a/src/infrastructure/daemon/workers/editor/PhabricatorWorkerBulkJobEditor.php b/src/infrastructure/daemon/workers/editor/PhabricatorWorkerBulkJobEditor.php
index b23c987d6..e94ca6dc4 100644
--- a/src/infrastructure/daemon/workers/editor/PhabricatorWorkerBulkJobEditor.php
+++ b/src/infrastructure/daemon/workers/editor/PhabricatorWorkerBulkJobEditor.php
@@ -1,87 +1,88 @@
<?php
final class PhabricatorWorkerBulkJobEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorDaemonsApplication';
}
public function getEditorObjectsDescription() {
return pht('Bulk Jobs');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorWorkerBulkJobTransaction::TYPE_STATUS;
+ $types[] = PhabricatorTransactions::TYPE_EDGE;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorWorkerBulkJobTransaction::TYPE_STATUS:
return $object->getStatus();
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorWorkerBulkJobTransaction::TYPE_STATUS:
return $xaction->getNewValue();
}
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$new = $xaction->getNewValue();
switch ($type) {
case PhabricatorWorkerBulkJobTransaction::TYPE_STATUS:
$object->setStatus($xaction->getNewValue());
return;
}
return parent::applyCustomInternalTransaction($object, $xaction);
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$new = $xaction->getNewValue();
switch ($type) {
case PhabricatorWorkerBulkJobTransaction::TYPE_STATUS:
switch ($new) {
case PhabricatorWorkerBulkJob::STATUS_WAITING:
PhabricatorWorker::scheduleTask(
'PhabricatorWorkerBulkJobCreateWorker',
array(
'jobID' => $object->getID(),
),
array(
'priority' => PhabricatorWorker::PRIORITY_BULK,
));
break;
}
return;
}
return parent::applyCustomExternalTransaction($object, $xaction);
}
}
diff --git a/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementExecuteWorkflow.php b/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementExecuteWorkflow.php
index 2acc8452e..f3c6be520 100644
--- a/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementExecuteWorkflow.php
+++ b/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementExecuteWorkflow.php
@@ -1,63 +1,104 @@
<?php
final class PhabricatorWorkerManagementExecuteWorkflow
extends PhabricatorWorkerManagementWorkflow {
protected function didConstruct() {
$this
->setName('execute')
->setExamples('**execute** --id __id__')
->setSynopsis(
pht(
'Execute a task explicitly. This command ignores leases, is '.
'dangerous, and may cause work to be performed twice.'))
- ->setArguments($this->getTaskSelectionArguments());
+ ->setArguments(
+ array_merge(
+ array(
+ array(
+ 'name' => 'retry',
+ 'help' => pht('Retry archived tasks.'),
+ ),
+ array(
+ 'name' => 'repeat',
+ 'help' => pht('Repeat archived, successful tasks.'),
+ ),
+ ),
+ $this->getTaskSelectionArguments()));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$tasks = $this->loadTasks($args);
+ $is_retry = $args->getArg('retry');
+ $is_repeat = $args->getArg('repeat');
+
foreach ($tasks as $task) {
$can_execute = !$task->isArchived();
if (!$can_execute) {
- $console->writeOut(
+ if (!$is_retry) {
+ $console->writeOut(
+ "**<bg:yellow> %s </bg>** %s\n",
+ pht('ARCHIVED'),
+ pht(
+ '%s is already archived, and will not be executed. '.
+ 'Use "--retry" to execute archived tasks.',
+ $this->describeTask($task)));
+ continue;
+ }
+
+ $result_success = PhabricatorWorkerArchiveTask::RESULT_SUCCESS;
+ if ($task->getResult() == $result_success) {
+ if (!$is_repeat) {
+ $console->writeOut(
+ "**<bg:yellow> %s </bg>** %s\n",
+ pht('SUCCEEDED'),
+ pht(
+ '%s has already succeeded, and will not be retried. '.
+ 'Use "--repeat" to repeat successful tasks.',
+ $this->describeTask($task)));
+ continue;
+ }
+ }
+
+ echo tsprintf(
"**<bg:yellow> %s </bg>** %s\n",
pht('ARCHIVED'),
pht(
- '%s is already archived, and can not be executed.',
+ 'Unarchiving %s.',
$this->describeTask($task)));
- continue;
+
+ $task = $task->unarchiveTask();
}
// NOTE: This ignores leases, maybe it should respect them without
// a parameter like --force?
$task->setLeaseOwner(null);
$task->setLeaseExpires(PhabricatorTime::getNow());
$task->save();
$task_data = id(new PhabricatorWorkerTaskData())->loadOneWhere(
'id = %d',
$task->getDataID());
$task->setData($task_data->getData());
echo tsprintf(
"%s\n",
pht(
'Executing task %d (%s)...',
$task->getID(),
$task->getTaskClass()));
$task = $task->executeTask();
$ex = $task->getExecutionException();
if ($ex) {
throw $ex;
}
}
return 0;
}
}
diff --git a/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementRetryWorkflow.php b/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementRetryWorkflow.php
index 6dbebd168..538a70add 100644
--- a/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementRetryWorkflow.php
+++ b/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementRetryWorkflow.php
@@ -1,57 +1,69 @@
<?php
final class PhabricatorWorkerManagementRetryWorkflow
extends PhabricatorWorkerManagementWorkflow {
protected function didConstruct() {
$this
->setName('retry')
->setExamples('**retry** --id __id__')
->setSynopsis(
pht(
'Retry selected tasks which previously failed permanently or '.
- 'were cancelled. Only archived, unsuccessful tasks can be '.
- 'retried.'))
- ->setArguments($this->getTaskSelectionArguments());
+ 'were cancelled. Only archived tasks can be retried.'))
+ ->setArguments(
+ array_merge(
+ array(
+ array(
+ 'name' => 'repeat',
+ 'help' => pht(
+ 'Repeat tasks which already completed successfully.'),
+ ),
+ ),
+ $this->getTaskSelectionArguments()));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$tasks = $this->loadTasks($args);
+ $is_repeat = $args->getArg('repeat');
foreach ($tasks as $task) {
if (!$task->isArchived()) {
$console->writeOut(
"**<bg:yellow> %s </bg>** %s\n",
pht('ACTIVE'),
pht(
'%s is already in the active task queue.',
$this->describeTask($task)));
continue;
}
$result_success = PhabricatorWorkerArchiveTask::RESULT_SUCCESS;
if ($task->getResult() == $result_success) {
- $console->writeOut(
- "**<bg:yellow> %s </bg>** %s\n",
- pht('SUCCEEDED'),
- pht(
- '%s has already succeeded, and can not be retried.',
- $this->describeTask($task)));
- continue;
+ if (!$is_repeat) {
+ $console->writeOut(
+ "**<bg:yellow> %s </bg>** %s\n",
+ pht('SUCCEEDED'),
+ pht(
+ '%s has already succeeded, and will not be repeated. '.
+ 'Use "--repeat" to repeat successful tasks.',
+ $this->describeTask($task)));
+ continue;
+ }
}
$task->unarchiveTask();
$console->writeOut(
"**<bg:green> %s </bg>** %s\n",
pht('QUEUED'),
pht(
'%s was queued for retry.',
$this->describeTask($task)));
}
return 0;
}
}
diff --git a/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php b/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php
index 87c16a48c..8ca12d60e 100644
--- a/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php
+++ b/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php
@@ -1,237 +1,241 @@
<?php
final class PhabricatorWorkerTriggerQuery
extends PhabricatorPolicyAwareQuery {
// NOTE: This is a PolicyAware query so it can work with other infrastructure
// like handles; triggers themselves are low-level and do not have
// meaningful policies.
const ORDER_ID = 'id';
const ORDER_EXECUTION = 'execution';
const ORDER_VERSION = 'version';
private $ids;
private $phids;
private $versionMin;
private $versionMax;
private $nextEpochMin;
private $nextEpochMax;
private $needEvents;
private $order = self::ORDER_ID;
public function getQueryApplicationClass() {
return null;
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withVersionBetween($min, $max) {
$this->versionMin = $min;
$this->versionMax = $max;
return $this;
}
public function withNextEventBetween($min, $max) {
$this->nextEpochMin = $min;
$this->nextEpochMax = $max;
return $this;
}
public function needEvents($need_events) {
$this->needEvents = $need_events;
return $this;
}
/**
* Set the result order.
*
* Note that using `ORDER_EXECUTION` will also filter results to include only
* triggers which have been scheduled to execute. You should not use this
* ordering when querying for specific triggers, e.g. by ID or PHID.
*
* @param const Result order.
* @return this
*/
public function setOrder($order) {
$this->order = $order;
return $this;
}
protected function nextPage(array $page) {
// NOTE: We don't implement paging because we don't currently ever need
// it and paging ORDER_EXECUTION is a hassle.
- throw new PhutilMethodNotImplementedException();
+
+ // (Before T13266, we raised an exception here, but since "nextPage()" is
+ // now called even if we don't page we can't do that anymore. Just do
+ // nothing instead.)
+ return null;
}
protected function loadPage() {
$task_table = new PhabricatorWorkerTrigger();
$conn_r = $task_table->establishConnection('r');
$rows = queryfx_all(
$conn_r,
'SELECT t.* FROM %T t %Q %Q %Q %Q',
$task_table->getTableName(),
$this->buildJoinClause($conn_r),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
$triggers = $task_table->loadAllFromArray($rows);
if ($triggers) {
if ($this->needEvents) {
$ids = mpull($triggers, 'getID');
$events = id(new PhabricatorWorkerTriggerEvent())->loadAllWhere(
'triggerID IN (%Ld)',
$ids);
$events = mpull($events, null, 'getTriggerID');
foreach ($triggers as $key => $trigger) {
$event = idx($events, $trigger->getID());
$trigger->attachEvent($event);
}
}
foreach ($triggers as $key => $trigger) {
$clock_class = $trigger->getClockClass();
if (!is_subclass_of($clock_class, 'PhabricatorTriggerClock')) {
unset($triggers[$key]);
continue;
}
try {
$argv = array($trigger->getClockProperties());
$clock = newv($clock_class, $argv);
} catch (Exception $ex) {
unset($triggers[$key]);
continue;
}
$trigger->attachClock($clock);
}
foreach ($triggers as $key => $trigger) {
$action_class = $trigger->getActionClass();
if (!is_subclass_of($action_class, 'PhabricatorTriggerAction')) {
unset($triggers[$key]);
continue;
}
try {
$argv = array($trigger->getActionProperties());
$action = newv($action_class, $argv);
} catch (Exception $ex) {
unset($triggers[$key]);
continue;
}
$trigger->attachAction($action);
}
}
return $triggers;
}
protected function buildJoinClause(AphrontDatabaseConnection $conn) {
$joins = array();
if (($this->nextEpochMin !== null) ||
($this->nextEpochMax !== null) ||
($this->order == self::ORDER_EXECUTION)) {
$joins[] = qsprintf(
$conn,
'JOIN %T e ON e.triggerID = t.id',
id(new PhabricatorWorkerTriggerEvent())->getTableName());
}
if ($joins) {
return qsprintf($conn, '%LJ', $joins);
} else {
return qsprintf($conn, '');
}
}
protected function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
't.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
't.phid IN (%Ls)',
$this->phids);
}
if ($this->versionMin !== null) {
$where[] = qsprintf(
$conn,
't.triggerVersion >= %d',
$this->versionMin);
}
if ($this->versionMax !== null) {
$where[] = qsprintf(
$conn,
't.triggerVersion <= %d',
$this->versionMax);
}
if ($this->nextEpochMin !== null) {
$where[] = qsprintf(
$conn,
'e.nextEventEpoch >= %d',
$this->nextEpochMin);
}
if ($this->nextEpochMax !== null) {
$where[] = qsprintf(
$conn,
'e.nextEventEpoch <= %d',
$this->nextEpochMax);
}
return $this->formatWhereClause($conn, $where);
}
private function buildOrderClause(AphrontDatabaseConnection $conn_r) {
switch ($this->order) {
case self::ORDER_ID:
return qsprintf(
$conn_r,
'ORDER BY id DESC');
case self::ORDER_EXECUTION:
return qsprintf(
$conn_r,
'ORDER BY e.nextEventEpoch ASC, e.id ASC');
case self::ORDER_VERSION:
return qsprintf(
$conn_r,
'ORDER BY t.triggerVersion ASC');
default:
throw new Exception(
pht(
'Unsupported order "%s".',
$this->order));
}
}
}
diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php
index ed1eeaea6..7139e39ac 100644
--- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php
+++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php
@@ -1,228 +1,215 @@
<?php
final class PhabricatorWorkerActiveTask extends PhabricatorWorkerTask {
protected $failureTime;
private $serverTime;
private $localTime;
protected function getConfiguration() {
$parent = parent::getConfiguration();
$config = array(
self::CONFIG_IDS => self::IDS_COUNTER,
- self::CONFIG_TIMESTAMPS => false,
self::CONFIG_KEY_SCHEMA => array(
- 'dataID' => array(
- 'columns' => array('dataID'),
- 'unique' => true,
- ),
'taskClass' => array(
'columns' => array('taskClass'),
),
'leaseExpires' => array(
'columns' => array('leaseExpires'),
),
- 'leaseOwner' => array(
- 'columns' => array('leaseOwner(16)'),
- ),
'key_failuretime' => array(
'columns' => array('failureTime'),
),
- 'leaseOwner_2' => array(
+ 'key_owner' => array(
'columns' => array('leaseOwner', 'priority', 'id'),
),
) + $parent[self::CONFIG_KEY_SCHEMA],
);
- $config[self::CONFIG_COLUMN_SCHEMA] = array(
- // T6203/NULLABILITY
- // This isn't nullable in the archive table, so at a minimum these
- // should be the same.
- 'dataID' => 'uint32?',
- ) + $parent[self::CONFIG_COLUMN_SCHEMA];
-
return $config + $parent;
}
public function setServerTime($server_time) {
$this->serverTime = $server_time;
$this->localTime = time();
return $this;
}
public function setLeaseDuration($lease_duration) {
$this->checkLease();
$server_lease_expires = $this->serverTime + $lease_duration;
$this->setLeaseExpires($server_lease_expires);
// NOTE: This is primarily to allow unit tests to set negative lease
// durations so they don't have to wait around for leases to expire. We
// check that the lease is valid above.
return $this->forceSaveWithoutLease();
}
public function save() {
$this->checkLease();
return $this->forceSaveWithoutLease();
}
public function forceSaveWithoutLease() {
$is_new = !$this->getID();
if ($is_new) {
$this->failureCount = 0;
}
- if ($is_new && ($this->getData() !== null)) {
+ if ($is_new) {
$data = new PhabricatorWorkerTaskData();
$data->setData($this->getData());
$data->save();
$this->setDataID($data->getID());
}
return parent::save();
}
protected function checkLease() {
$owner = $this->leaseOwner;
if (!$owner) {
return;
}
if ($owner == PhabricatorWorker::YIELD_OWNER) {
return;
}
$current_server_time = $this->serverTime + (time() - $this->localTime);
if ($current_server_time >= $this->leaseExpires) {
throw new Exception(
pht(
'Trying to update Task %d (%s) after lease expiration!',
$this->getID(),
$this->getTaskClass()));
}
}
public function delete() {
throw new Exception(
pht(
'Active tasks can not be deleted directly. '.
'Use %s to move tasks to the archive.',
'archiveTask()'));
}
public function archiveTask($result, $duration) {
if ($this->getID() === null) {
throw new Exception(
pht("Attempting to archive a task which hasn't been saved!"));
}
$this->checkLease();
$archive = id(new PhabricatorWorkerArchiveTask())
->setID($this->getID())
->setTaskClass($this->getTaskClass())
->setLeaseOwner($this->getLeaseOwner())
->setLeaseExpires($this->getLeaseExpires())
->setFailureCount($this->getFailureCount())
->setDataID($this->getDataID())
->setPriority($this->getPriority())
->setObjectPHID($this->getObjectPHID())
->setResult($result)
- ->setDuration($duration);
+ ->setDuration($duration)
+ ->setDateCreated($this->getDateCreated())
+ ->setArchivedEpoch(PhabricatorTime::getNow());
// NOTE: This deletes the active task (this object)!
$archive->save();
return $archive;
}
public function executeTask() {
// We do this outside of the try .. catch because we don't have permission
// to release the lease otherwise.
$this->checkLease();
$did_succeed = false;
$worker = null;
try {
$worker = $this->getWorkerInstance();
$worker->setCurrentWorkerTask($this);
$maximum_failures = $worker->getMaximumRetryCount();
if ($maximum_failures !== null) {
if ($this->getFailureCount() > $maximum_failures) {
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Task %d has exceeded the maximum number of failures (%d).',
$this->getID(),
$maximum_failures));
}
}
$lease = $worker->getRequiredLeaseTime();
if ($lease !== null) {
$this->setLeaseDuration($lease);
}
$t_start = microtime(true);
$worker->executeTask();
$duration = phutil_microseconds_since($t_start);
$result = $this->archiveTask(
PhabricatorWorkerArchiveTask::RESULT_SUCCESS,
$duration);
$did_succeed = true;
} catch (PhabricatorWorkerPermanentFailureException $ex) {
$result = $this->archiveTask(
PhabricatorWorkerArchiveTask::RESULT_FAILURE,
0);
$result->setExecutionException($ex);
} catch (PhabricatorWorkerYieldException $ex) {
$this->setExecutionException($ex);
$this->setLeaseOwner(PhabricatorWorker::YIELD_OWNER);
$retry = $ex->getDuration();
$retry = max($retry, 5);
// NOTE: As a side effect, this saves the object.
$this->setLeaseDuration($retry);
$result = $this;
} catch (Exception $ex) {
$this->setExecutionException($ex);
$this->setFailureCount($this->getFailureCount() + 1);
$this->setFailureTime(time());
$retry = null;
if ($worker) {
$retry = $worker->getWaitBeforeRetry($this);
}
$retry = coalesce(
$retry,
PhabricatorWorkerLeaseQuery::getDefaultWaitBeforeRetry());
// NOTE: As a side effect, this saves the object.
$this->setLeaseDuration($retry);
$result = $this;
}
// NOTE: If this throws, we don't want it to cause the task to fail again,
// so execute it out here and just let the exception escape.
if ($did_succeed) {
// Default the new task priority to our own priority.
$defaults = array(
'priority' => (int)$this->getPriority(),
);
$worker->flushTaskQueue($defaults);
}
return $result;
}
}
diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php
index fe1164e53..25a453b47 100644
--- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php
+++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php
@@ -1,100 +1,100 @@
<?php
final class PhabricatorWorkerArchiveTask extends PhabricatorWorkerTask {
const RESULT_SUCCESS = 0;
const RESULT_FAILURE = 1;
const RESULT_CANCELLED = 2;
protected $duration;
protected $result;
+ protected $archivedEpoch;
protected function getConfiguration() {
$parent = parent::getConfiguration();
$config = array(
// We manage the IDs in this table; they are allocated in the ActiveTask
// table and moved here without alteration.
self::CONFIG_IDS => self::IDS_MANUAL,
) + $parent;
$config[self::CONFIG_COLUMN_SCHEMA] = array(
'result' => 'uint32',
'duration' => 'uint64',
+ 'archivedEpoch' => 'epoch?',
) + $config[self::CONFIG_COLUMN_SCHEMA];
$config[self::CONFIG_KEY_SCHEMA] = array(
'dateCreated' => array(
'columns' => array('dateCreated'),
),
- 'leaseOwner' => array(
- 'columns' => array('leaseOwner', 'priority', 'id'),
- ),
'key_modified' => array(
'columns' => array('dateModified'),
),
) + $parent[self::CONFIG_KEY_SCHEMA];
return $config;
}
public function save() {
if ($this->getID() === null) {
throw new Exception(pht('Trying to archive a task with no ID.'));
}
$other = new PhabricatorWorkerActiveTask();
$conn_w = $this->establishConnection('w');
$this->openTransaction();
queryfx(
$conn_w,
'DELETE FROM %T WHERE id = %d',
$other->getTableName(),
$this->getID());
$result = parent::insert();
$this->saveTransaction();
return $result;
}
public function delete() {
$this->openTransaction();
if ($this->getDataID()) {
$conn_w = $this->establishConnection('w');
$data_table = new PhabricatorWorkerTaskData();
queryfx(
$conn_w,
'DELETE FROM %T WHERE id = %d',
$data_table->getTableName(),
$this->getDataID());
}
$result = parent::delete();
$this->saveTransaction();
return $result;
}
public function unarchiveTask() {
$this->openTransaction();
$active = id(new PhabricatorWorkerActiveTask())
->setID($this->getID())
->setTaskClass($this->getTaskClass())
->setLeaseOwner(null)
->setLeaseExpires(0)
->setFailureCount(0)
->setDataID($this->getDataID())
->setPriority($this->getPriority())
->setObjectPHID($this->getObjectPHID())
+ ->setDateCreated($this->getDateCreated())
->insert();
$this->setDataID(null);
$this->delete();
$this->saveTransaction();
return $active;
}
}
diff --git a/src/infrastructure/diff/PhabricatorDiffScopeEngine.php b/src/infrastructure/diff/PhabricatorDiffScopeEngine.php
new file mode 100644
index 000000000..5ea1ec502
--- /dev/null
+++ b/src/infrastructure/diff/PhabricatorDiffScopeEngine.php
@@ -0,0 +1,156 @@
+<?php
+
+final class PhabricatorDiffScopeEngine
+ extends Phobject {
+
+ private $lineTextMap;
+ private $lineDepthMap;
+
+ public function setLineTextMap(array $map) {
+ if (array_key_exists(0, $map)) {
+ throw new Exception(
+ pht('ScopeEngine text map must be a 1-based map of lines.'));
+ }
+
+ $expect = 1;
+ foreach ($map as $key => $value) {
+ if ($key === $expect) {
+ $expect++;
+ continue;
+ }
+
+ throw new Exception(
+ pht(
+ 'ScopeEngine text map must be a contiguous map of '.
+ 'lines, but is not: found key "%s" where key "%s" was expected.',
+ $key,
+ $expect));
+ }
+
+ $this->lineTextMap = $map;
+
+ return $this;
+ }
+
+ public function getLineTextMap() {
+ if ($this->lineTextMap === null) {
+ throw new PhutilInvalidStateException('setLineTextMap');
+ }
+ return $this->lineTextMap;
+ }
+
+ public function getScopeStart($line) {
+ $text_map = $this->getLineTextMap();
+ $depth_map = $this->getLineDepthMap();
+ $length = count($text_map);
+
+ // Figure out the effective depth of the line we're getting scope for.
+ // If the line is just whitespace, it may have no depth on its own. In
+ // this case, we look for the next line.
+ $line_depth = null;
+ for ($ii = $line; $ii <= $length; $ii++) {
+ if ($depth_map[$ii] !== null) {
+ $line_depth = $depth_map[$ii];
+ break;
+ }
+ }
+
+ // If we can't find a line depth for the target line, just bail.
+ if ($line_depth === null) {
+ return null;
+ }
+
+ // Limit the maximum number of lines we'll examine. If a user has a
+ // million-line diff of nonsense, scanning the whole thing is a waste
+ // of time.
+ $search_range = 1000;
+ $search_until = max(0, $ii - $search_range);
+
+ for ($ii = $line - 1; $ii > $search_until; $ii--) {
+ $line_text = $text_map[$ii];
+
+ // This line is in missing context: the diff was diffed with partial
+ // context, and we ran out of context before finding a good scope line.
+ // Bail out, we don't want to jump across missing context blocks.
+ if ($line_text === null) {
+ return null;
+ }
+
+ $depth = $depth_map[$ii];
+
+ // This line is all whitespace. This isn't a possible match.
+ if ($depth === null) {
+ continue;
+ }
+
+ // The depth is the same as (or greater than) the depth we started with,
+ // so this isn't a possible match.
+ if ($depth >= $line_depth) {
+ continue;
+ }
+
+ // Reject lines which begin with "}" or "{". These lines are probably
+ // never good matches.
+ if (preg_match('/^\s*[{}]/i', $line_text)) {
+ continue;
+ }
+
+ return $ii;
+ }
+
+ return null;
+ }
+
+ private function getLineDepthMap() {
+ if (!$this->lineDepthMap) {
+ $this->lineDepthMap = $this->newLineDepthMap();
+ }
+
+ return $this->lineDepthMap;
+ }
+
+ private function newLineDepthMap() {
+ $text_map = $this->getLineTextMap();
+
+ // TODO: This should be configurable once we handle tab widths better.
+ $tab_width = 2;
+
+ $depth_map = array();
+ foreach ($text_map as $line_number => $line_text) {
+ if ($line_text === null) {
+ $depth_map[$line_number] = null;
+ continue;
+ }
+
+ $len = strlen($line_text);
+
+ // If the line has no actual text, don't assign it a depth.
+ if (!$len || !strlen(trim($line_text))) {
+ $depth_map[$line_number] = null;
+ continue;
+ }
+
+ $count = 0;
+ for ($ii = 0; $ii < $len; $ii++) {
+ $c = $line_text[$ii];
+ if ($c == ' ') {
+ $count++;
+ } else if ($c == "\t") {
+ $count += $tab_width;
+ } else {
+ break;
+ }
+ }
+
+ // Round down to cheat our way through the " *" parts of docblock
+ // comments. This is generally a reasonble heuristic because odd tab
+ // widths are exceptionally rare.
+ $depth = ($count >> 1);
+
+ $depth_map[$line_number] = $depth;
+ }
+
+ return $depth_map;
+ }
+
+}
diff --git a/src/infrastructure/diff/PhabricatorDifferenceEngine.php b/src/infrastructure/diff/PhabricatorDifferenceEngine.php
index 3b4eb5547..84e88ceaa 100644
--- a/src/infrastructure/diff/PhabricatorDifferenceEngine.php
+++ b/src/infrastructure/diff/PhabricatorDifferenceEngine.php
@@ -1,171 +1,172 @@
<?php
/**
* Utility class which encapsulates some shared behavior between different
* applications which render diffs.
*
* @task config Configuring the Engine
* @task diff Generating Diffs
*/
final class PhabricatorDifferenceEngine extends Phobject {
- private $ignoreWhitespace;
private $oldName;
private $newName;
+ private $normalize;
/* -( Configuring the Engine )--------------------------------------------- */
- /**
- * If true, ignore whitespace when computing differences.
- *
- * @param bool Ignore whitespace?
- * @return this
- * @task config
- */
- public function setIgnoreWhitespace($ignore_whitespace) {
- $this->ignoreWhitespace = $ignore_whitespace;
- return $this;
- }
-
-
/**
* Set the name to identify the old file with. Primarily cosmetic.
*
* @param string Old file name.
* @return this
* @task config
*/
public function setOldName($old_name) {
$this->oldName = $old_name;
return $this;
}
/**
* Set the name to identify the new file with. Primarily cosmetic.
*
* @param string New file name.
* @return this
* @task config
*/
public function setNewName($new_name) {
$this->newName = $new_name;
return $this;
}
+ public function setNormalize($normalize) {
+ $this->normalize = $normalize;
+ return $this;
+ }
+
+ public function getNormalize() {
+ return $this->normalize;
+ }
+
+
/* -( Generating Diffs )--------------------------------------------------- */
/**
* Generate a raw diff from two raw files. This is a lower-level API than
* @{method:generateChangesetFromFileContent}, but may be useful if you need
* to use a custom parser configuration, as with Diffusion.
*
* @param string Entire previous file content.
* @param string Entire current file content.
* @return string Raw diff between the two files.
* @task diff
*/
public function generateRawDiffFromFileContent($old, $new) {
$options = array();
- if ($this->ignoreWhitespace) {
- $options[] = '-bw';
- }
// Generate diffs with full context.
$options[] = '-U65535';
$old_name = nonempty($this->oldName, '/dev/universe').' 9999-99-99';
$new_name = nonempty($this->newName, '/dev/universe').' 9999-99-99';
$options[] = '-L';
$options[] = $old_name;
$options[] = '-L';
$options[] = $new_name;
+ $normalize = $this->getNormalize();
+ if ($normalize) {
+ $old = $this->normalizeFile($old);
+ $new = $this->normalizeFile($new);
+ }
+
$old_tmp = new TempFile();
$new_tmp = new TempFile();
Filesystem::writeFile($old_tmp, $old);
Filesystem::writeFile($new_tmp, $new);
list($err, $diff) = exec_manual(
'diff %Ls %s %s',
$options,
$old_tmp,
$new_tmp);
if (!$err) {
- // This indicates that the two files are the same (or, possibly, the
- // same modulo whitespace differences, which is why we can't do this
- // check trivially before running `diff`). Build a synthetic, changeless
- // diff so that we can still render the raw, unchanged file instead of
- // being forced to just say "this file didn't change" since we don't have
- // the content.
+ // This indicates that the two files are the same. Build a synthetic,
+ // changeless diff so that we can still render the raw, unchanged file
+ // instead of being forced to just say "this file didn't change" since we
+ // don't have the content.
$entire_file = explode("\n", $old);
foreach ($entire_file as $k => $line) {
$entire_file[$k] = ' '.$line;
}
$len = count($entire_file);
$entire_file = implode("\n", $entire_file);
// TODO: If both files were identical but missing newlines, we probably
// get this wrong. Unclear if it ever matters.
// This is a bit hacky but the diff parser can handle it.
$diff = "--- {$old_name}\n".
"+++ {$new_name}\n".
"@@ -1,{$len} +1,{$len} @@\n".
$entire_file."\n";
- } else {
- if ($this->ignoreWhitespace) {
-
- // Under "-bw", `diff` is inconsistent about emitting "\ No newline
- // at end of file". For instance, a long file with a change in the
- // middle will emit a contextless "\ No newline..." at the end if a
- // newline is removed, but not if one is added. A file with a change
- // at the end will emit the "old" "\ No newline..." block only, even
- // if the newline was not removed. Since we're ostensibly ignoring
- // whitespace changes, just drop these lines if they appear anywhere
- // in the diff.
-
- $lines = explode("\n", $diff);
- foreach ($lines as $key => $line) {
- if (isset($line[0]) && $line[0] == '\\') {
- unset($lines[$key]);
- }
- }
- $diff = implode("\n", $lines);
- }
}
return $diff;
}
/**
* Generate an @{class:DifferentialChangeset} from two raw files. This is
* principally useful because you can feed the output to
* @{class:DifferentialChangesetParser} in order to render it.
*
* @param string Entire previous file content.
* @param string Entire current file content.
* @return @{class:DifferentialChangeset} Synthetic changeset.
* @task diff
*/
public function generateChangesetFromFileContent($old, $new) {
$diff = $this->generateRawDiffFromFileContent($old, $new);
$changes = id(new ArcanistDiffParser())->parseDiff($diff);
$diff = DifferentialDiff::newEphemeralFromRawChanges(
$changes);
return head($diff->getChangesets());
}
+ private function normalizeFile($corpus) {
+ // We can freely apply any other transformations we want to here: we have
+ // no constraints on what we need to preserve. If we normalize every line
+ // to "cat", the diff will still work, the alignment of the "-" / "+"
+ // lines will just be very hard to read.
+
+ // In general, we'll make the diff better if we normalize two lines that
+ // humans think are the same.
+
+ // We'll make the diff worse if we normalize two lines that humans think
+ // are different.
+
+
+ // Strip all whitespace present anywhere in the diff, since humans never
+ // consider whitespace changes to alter the line into a "different line"
+ // even when they're semantic (e.g., in a string constant). This covers
+ // indentation changes, trailing whitepspace, and formatting changes
+ // like "+/-".
+ $corpus = preg_replace('/[ \t]/', '', $corpus);
+
+ return $corpus;
+ }
+
}
diff --git a/src/infrastructure/diff/__tests__/PhabricatorDiffScopeEngineTestCase.php b/src/infrastructure/diff/__tests__/PhabricatorDiffScopeEngineTestCase.php
new file mode 100644
index 000000000..50e23ac31
--- /dev/null
+++ b/src/infrastructure/diff/__tests__/PhabricatorDiffScopeEngineTestCase.php
@@ -0,0 +1,51 @@
+<?php
+
+final class PhabricatorDiffScopeEngineTestCase
+ extends PhabricatorTestCase {
+
+ private $engines = array();
+
+ public function testScopeEngine() {
+ $this->assertScopeStart('zebra.c', 4, 2);
+ }
+
+ private function assertScopeStart($file, $line, $expect) {
+ $engine = $this->getScopeTestEngine($file);
+
+ $actual = $engine->getScopeStart($line);
+ $this->assertEqual(
+ $expect,
+ $actual,
+ pht(
+ 'Expect scope for line %s to start on line %s (actual: %s) in "%s".',
+ $line,
+ $expect,
+ $actual,
+ $file));
+ }
+
+ private function getScopeTestEngine($file) {
+ if (!isset($this->engines[$file])) {
+ $this->engines[$file] = $this->newScopeTestEngine($file);
+ }
+
+ return $this->engines[$file];
+ }
+
+ private function newScopeTestEngine($file) {
+ $path = dirname(__FILE__).'/data/'.$file;
+ $data = Filesystem::readFile($path);
+
+ $lines = phutil_split_lines($data);
+ $map = array();
+ foreach ($lines as $key => $line) {
+ $map[$key + 1] = $line;
+ }
+
+ $engine = id(new PhabricatorDiffScopeEngine())
+ ->setLineTextMap($map);
+
+ return $engine;
+ }
+
+}
diff --git a/src/infrastructure/diff/__tests__/data/zebra.c b/src/infrastructure/diff/__tests__/data/zebra.c
new file mode 100644
index 000000000..d587b018a
--- /dev/null
+++ b/src/infrastructure/diff/__tests__/data/zebra.c
@@ -0,0 +1,5 @@
+void
+ZebraTamer::TameAZebra(nsPoint where, const nsRect& zone, nsAtom* material)
+{
+ zebra.tame = true;
+}
diff --git a/src/infrastructure/diff/view/PHUIDiffOneUpInlineCommentRowScaffold.php b/src/infrastructure/diff/view/PHUIDiffOneUpInlineCommentRowScaffold.php
index 53c2255dc..fe5cab862 100644
--- a/src/infrastructure/diff/view/PHUIDiffOneUpInlineCommentRowScaffold.php
+++ b/src/infrastructure/diff/view/PHUIDiffOneUpInlineCommentRowScaffold.php
@@ -1,42 +1,41 @@
<?php
/**
* Row scaffold for `1up` (unified) changeset views.
*
* This scaffold is straightforward.
*/
final class PHUIDiffOneUpInlineCommentRowScaffold
extends PHUIDiffInlineCommentRowScaffold {
public function render() {
$inlines = $this->getInlineViews();
if (count($inlines) != 1) {
throw new Exception(
pht('One-up inline row scaffold must have exactly one inline view!'));
}
$inline = head($inlines);
$attrs = array(
'colspan' => 3,
- 'class' => 'right3',
'id' => $inline->getScaffoldCellID(),
);
if ($inline->getIsOnRight()) {
$left_hidden = null;
$right_hidden = $inline->newHiddenIcon();
} else {
$left_hidden = $inline->newHiddenIcon();
$right_hidden = null;
}
$cells = array(
- phutil_tag('th', array(), $left_hidden),
- phutil_tag('th', array(), $right_hidden),
+ phutil_tag('td', array('class' => 'n'), $left_hidden),
+ phutil_tag('td', array('class' => 'n'), $right_hidden),
phutil_tag('td', $attrs, $inline),
);
return javelin_tag('tr', $this->getRowAttributes(), $cells);
}
}
diff --git a/src/infrastructure/diff/view/PHUIDiffTwoUpInlineCommentRowScaffold.php b/src/infrastructure/diff/view/PHUIDiffTwoUpInlineCommentRowScaffold.php
index 81b0edaf4..f9bde17bf 100644
--- a/src/infrastructure/diff/view/PHUIDiffTwoUpInlineCommentRowScaffold.php
+++ b/src/infrastructure/diff/view/PHUIDiffTwoUpInlineCommentRowScaffold.php
@@ -1,83 +1,83 @@
<?php
/**
* Row scaffold for 2up (side-by-side) changeset views.
*
* Although this scaffold is normally straightforward, it may also accept
* two inline comments and display them adjacently.
*/
final class PHUIDiffTwoUpInlineCommentRowScaffold
extends PHUIDiffInlineCommentRowScaffold {
public function render() {
$inlines = $this->getInlineViews();
if (!$inlines) {
throw new Exception(
pht('Two-up inline row scaffold must have at least one inline view.'));
}
if (count($inlines) > 2) {
throw new Exception(
pht('Two-up inline row scaffold must have at most two inline views.'));
}
if (count($inlines) == 1) {
$inline = head($inlines);
if ($inline->getIsOnRight()) {
$left_side = null;
$right_side = $inline;
$left_hidden = null;
$right_hidden = $inline->newHiddenIcon();
} else {
$left_side = $inline;
$right_side = null;
$left_hidden = $inline->newHiddenIcon();
$right_hidden = null;
}
} else {
list($u, $v) = $inlines;
if ($u->getIsOnRight() == $v->getIsOnRight()) {
throw new Exception(
pht(
'Two-up inline row scaffold must have one comment on the left and '.
'one comment on the right when showing two comments.'));
}
if ($v->getIsOnRight()) {
$left_side = $u;
$right_side = $v;
} else {
$left_side = $v;
$right_side = $u;
}
$left_hidden = null;
$right_hidden = null;
}
$left_attrs = array(
'class' => 'left',
'id' => ($left_side ? $left_side->getScaffoldCellID() : null),
);
$right_attrs = array(
- 'colspan' => 3,
- 'class' => 'right3',
+ 'colspan' => 2,
'id' => ($right_side ? $right_side->getScaffoldCellID() : null),
);
$cells = array(
- phutil_tag('th', array(), $left_hidden),
+ phutil_tag('td', array('class' => 'n'), $left_hidden),
phutil_tag('td', $left_attrs, $left_side),
- phutil_tag('th', array(), $right_hidden),
+ phutil_tag('td', array('class' => 'n'), $right_hidden),
+ phutil_tag('td', array('class' => 'copy')),
phutil_tag('td', $right_attrs, $right_side),
);
return javelin_tag('tr', $this->getRowAttributes(), $cells);
}
}
diff --git a/src/infrastructure/edges/conduit/PhabricatorEdgeObject.php b/src/infrastructure/edges/conduit/PhabricatorEdgeObject.php
index ec5f84e59..318318563 100644
--- a/src/infrastructure/edges/conduit/PhabricatorEdgeObject.php
+++ b/src/infrastructure/edges/conduit/PhabricatorEdgeObject.php
@@ -1,63 +1,76 @@
<?php
final class PhabricatorEdgeObject
extends Phobject
implements PhabricatorPolicyInterface {
private $id;
private $src;
private $dst;
private $type;
+ private $dateCreated;
+ private $sequence;
public static function newFromRow(array $row) {
$edge = new self();
- $edge->id = $row['id'];
- $edge->src = $row['src'];
- $edge->dst = $row['dst'];
- $edge->type = $row['type'];
+ $edge->id = idx($row, 'id');
+ $edge->src = idx($row, 'src');
+ $edge->dst = idx($row, 'dst');
+ $edge->type = idx($row, 'type');
+ $edge->dateCreated = idx($row, 'dateCreated');
+ $edge->sequence = idx($row, 'seq');
return $edge;
}
public function getID() {
return $this->id;
}
public function getSourcePHID() {
return $this->src;
}
public function getEdgeType() {
return $this->type;
}
public function getDestinationPHID() {
return $this->dst;
}
public function getPHID() {
return null;
}
+ public function getDateCreated() {
+ return $this->dateCreated;
+ }
+
+ public function getSequence() {
+ return $this->sequence;
+ }
+
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
}
diff --git a/src/infrastructure/edges/query/PhabricatorEdgeObjectQuery.php b/src/infrastructure/edges/query/PhabricatorEdgeObjectQuery.php
index 138521556..048a2a9fb 100644
--- a/src/infrastructure/edges/query/PhabricatorEdgeObjectQuery.php
+++ b/src/infrastructure/edges/query/PhabricatorEdgeObjectQuery.php
@@ -1,163 +1,182 @@
<?php
/**
* This is a more formal version of @{class:PhabricatorEdgeQuery} that is used
* to expose edges to Conduit.
*/
final class PhabricatorEdgeObjectQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $sourcePHIDs;
private $sourcePHIDType;
private $edgeTypes;
private $destinationPHIDs;
-
public function withSourcePHIDs(array $source_phids) {
$this->sourcePHIDs = $source_phids;
return $this;
}
public function withEdgeTypes(array $types) {
$this->edgeTypes = $types;
return $this;
}
public function withDestinationPHIDs(array $destination_phids) {
$this->destinationPHIDs = $destination_phids;
return $this;
}
protected function willExecute() {
$source_phids = $this->sourcePHIDs;
if (!$source_phids) {
throw new Exception(
pht(
'Edge object query must be executed with a nonempty list of '.
'source PHIDs.'));
}
$phid_item = null;
$phid_type = null;
foreach ($source_phids as $phid) {
$this_type = phid_get_type($phid);
if ($this_type == PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
throw new Exception(
pht(
'Source PHID "%s" in edge object query has unknown PHID type.',
$phid));
}
if ($phid_type === null) {
$phid_type = $this_type;
$phid_item = $phid;
continue;
}
if ($phid_type !== $this_type) {
throw new Exception(
pht(
'Two source PHIDs ("%s" and "%s") have different PHID types '.
'("%s" and "%s"). All PHIDs must be of the same type to execute '.
'an edge object query.',
$phid_item,
$phid,
$phid_type,
$this_type));
}
}
$this->sourcePHIDType = $phid_type;
}
protected function loadPage() {
$type = $this->sourcePHIDType;
$conn = PhabricatorEdgeConfig::establishConnection($type, 'r');
$table = PhabricatorEdgeConfig::TABLE_NAME_EDGE;
$rows = $this->loadStandardPageRowsWithConnection($conn, $table);
$result = array();
foreach ($rows as $row) {
$result[] = PhabricatorEdgeObject::newFromRow($row);
}
return $result;
}
- protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
- $parts = parent::buildSelectClauseParts($conn);
-
- // TODO: This is hacky, because we don't have real IDs on this table.
- $parts[] = qsprintf(
- $conn,
- 'CONCAT(dateCreated, %s, seq) AS id',
- '_');
-
- return $parts;
- }
-
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$parts = parent::buildWhereClauseParts($conn);
$parts[] = qsprintf(
$conn,
'src IN (%Ls)',
$this->sourcePHIDs);
$parts[] = qsprintf(
$conn,
'type IN (%Ls)',
$this->edgeTypes);
if ($this->destinationPHIDs !== null) {
$parts[] = qsprintf(
$conn,
'dst IN (%Ls)',
$this->destinationPHIDs);
}
return $parts;
}
public function getQueryApplicationClass() {
return null;
}
protected function getPrimaryTableAlias() {
return 'edge';
}
public function getOrderableColumns() {
return array(
'dateCreated' => array(
'table' => 'edge',
'column' => 'dateCreated',
'type' => 'int',
),
'sequence' => array(
'table' => 'edge',
'column' => 'seq',
'type' => 'int',
// TODO: This is not actually unique, but we're just doing our best
// here.
'unique' => true,
),
);
}
protected function getDefaultOrderVector() {
return array('dateCreated', 'sequence');
}
- protected function getPagingValueMap($cursor, array $keys) {
- $parts = explode('_', $cursor);
+ protected function newInternalCursorFromExternalCursor($cursor) {
+ list($epoch, $sequence) = $this->parseCursor($cursor);
+
+ // Instead of actually loading an edge, we're just making a fake edge
+ // with the properties the cursor describes.
+
+ $edge_object = PhabricatorEdgeObject::newFromRow(
+ array(
+ 'dateCreated' => $epoch,
+ 'seq' => $sequence,
+ ));
+ return id(new PhabricatorQueryCursor())
+ ->setObject($edge_object);
+ }
+
+ protected function newPagingMapFromPartialObject($object) {
return array(
- 'dateCreated' => $parts[0],
- 'sequence' => $parts[1],
+ 'dateCreated' => $object->getDateCreated(),
+ 'sequence' => $object->getSequence(),
);
}
+ protected function newExternalCursorStringForResult($object) {
+ return sprintf(
+ '%d_%d',
+ $object->getDateCreated(),
+ $object->getSequence());
+ }
+
+ private function parseCursor($cursor) {
+ if (!preg_match('/^\d+_\d+\z/', $cursor)) {
+ $this->throwCursorException(
+ pht(
+ 'Expected edge cursor in the form "0123_6789", got "%s".',
+ $cursor));
+ }
+
+ return explode('_', $cursor);
+ }
+
}
diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php
index 1236c620f..ff6a3db04 100644
--- a/src/infrastructure/env/PhabricatorEnv.php
+++ b/src/infrastructure/env/PhabricatorEnv.php
@@ -1,979 +1,985 @@
<?php
/**
* Manages the execution environment configuration, exposing APIs to read
* configuration settings and other similar values that are derived directly
* from configuration settings.
*
*
* = Reading Configuration =
*
* The primary role of this class is to provide an API for reading
* Phabricator configuration, @{method:getEnvConfig}:
*
* $value = PhabricatorEnv::getEnvConfig('some.key', $default);
*
* The class also handles some URI construction based on configuration, via
* the methods @{method:getURI}, @{method:getProductionURI},
* @{method:getCDNURI}, and @{method:getDoclink}.
*
* For configuration which allows you to choose a class to be responsible for
* some functionality (e.g., which mail adapter to use to deliver email),
* @{method:newObjectFromConfig} provides a simple interface that validates
* the configured value.
*
*
* = Unit Test Support =
*
* In unit tests, you can use @{method:beginScopedEnv} to create a temporary,
* mutable environment. The method returns a scope guard object which restores
* the environment when it is destroyed. For example:
*
* public function testExample() {
* $env = PhabricatorEnv::beginScopedEnv();
* $env->overrideEnv('some.key', 'new-value-for-this-test');
*
* // Some test which depends on the value of 'some.key'.
*
* }
*
* Your changes will persist until the `$env` object leaves scope or is
* destroyed.
*
* You should //not// use this in normal code.
*
*
* @task read Reading Configuration
* @task uri URI Validation
* @task test Unit Test Support
* @task internal Internals
*/
final class PhabricatorEnv extends Phobject {
private static $sourceStack;
private static $repairSource;
private static $overrideSource;
private static $requestBaseURI;
private static $cache;
private static $localeCode;
private static $readOnly;
private static $readOnlyReason;
const READONLY_CONFIG = 'config';
const READONLY_UNREACHABLE = 'unreachable';
const READONLY_SEVERED = 'severed';
const READONLY_MASTERLESS = 'masterless';
/**
* @phutil-external-symbol class PhabricatorStartup
*/
public static function initializeWebEnvironment() {
self::initializeCommonEnvironment(false);
}
public static function initializeScriptEnvironment($config_optional) {
self::initializeCommonEnvironment($config_optional);
// NOTE: This is dangerous in general, but we know we're in a script context
// and are not vulnerable to CSRF.
AphrontWriteGuard::allowDangerousUnguardedWrites(true);
// There are several places where we log information (about errors, events,
// service calls, etc.) for analysis via DarkConsole or similar. These are
// useful for web requests, but grow unboundedly in long-running scripts and
// daemons. Discard data as it arrives in these cases.
PhutilServiceProfiler::getInstance()->enableDiscardMode();
DarkConsoleErrorLogPluginAPI::enableDiscardMode();
DarkConsoleEventPluginAPI::enableDiscardMode();
}
private static function initializeCommonEnvironment($config_optional) {
PhutilErrorHandler::initialize();
self::resetUmask();
self::buildConfigurationSourceStack($config_optional);
// Force a valid timezone. If both PHP and Phabricator configuration are
// invalid, use UTC.
$tz = self::getEnvConfig('phabricator.timezone');
if ($tz) {
@date_default_timezone_set($tz);
}
$ok = @date_default_timezone_set(date_default_timezone_get());
if (!$ok) {
date_default_timezone_set('UTC');
}
// Prepend '/support/bin' and append any paths to $PATH if we need to.
$env_path = getenv('PATH');
$phabricator_path = dirname(phutil_get_library_root('phabricator'));
$support_path = $phabricator_path.'/support/bin';
$env_path = $support_path.PATH_SEPARATOR.$env_path;
$append_dirs = self::getEnvConfig('environment.append-paths');
if (!empty($append_dirs)) {
$append_path = implode(PATH_SEPARATOR, $append_dirs);
$env_path = $env_path.PATH_SEPARATOR.$append_path;
}
putenv('PATH='.$env_path);
// Write this back into $_ENV, too, so ExecFuture picks it up when creating
// subprocess environments.
$_ENV['PATH'] = $env_path;
// If an instance identifier is defined, write it into the environment so
// it's available to subprocesses.
$instance = self::getEnvConfig('cluster.instance');
if (strlen($instance)) {
putenv('PHABRICATOR_INSTANCE='.$instance);
$_ENV['PHABRICATOR_INSTANCE'] = $instance;
}
PhabricatorEventEngine::initialize();
// TODO: Add a "locale.default" config option once we have some reasonable
// defaults which aren't silly nonsense.
self::setLocaleCode('en_US');
}
public static function beginScopedLocale($locale_code) {
return new PhabricatorLocaleScopeGuard($locale_code);
}
public static function getLocaleCode() {
return self::$localeCode;
}
public static function setLocaleCode($locale_code) {
if (!$locale_code) {
return;
}
if ($locale_code == self::$localeCode) {
return;
}
try {
$locale = PhutilLocale::loadLocale($locale_code);
$translations = PhutilTranslation::getTranslationMapForLocale(
$locale_code);
$override = self::getEnvConfig('translation.override');
if (!is_array($override)) {
$override = array();
}
PhutilTranslator::getInstance()
->setLocale($locale)
->setTranslations($override + $translations);
self::$localeCode = $locale_code;
} catch (Exception $ex) {
// Just ignore this; the user likely has an out-of-date locale code.
}
}
private static function buildConfigurationSourceStack($config_optional) {
self::dropConfigCache();
$stack = new PhabricatorConfigStackSource();
self::$sourceStack = $stack;
$default_source = id(new PhabricatorConfigDefaultSource())
->setName(pht('Global Default'));
$stack->pushSource($default_source);
$env = self::getSelectedEnvironmentName();
if ($env) {
$stack->pushSource(
id(new PhabricatorConfigFileSource($env))
->setName(pht("File '%s'", $env)));
}
$stack->pushSource(
id(new PhabricatorConfigLocalSource())
->setName(pht('Local Config')));
// If the install overrides the database adapter, we might need to load
// the database adapter class before we can push on the database config.
// This config is locked and can't be edited from the web UI anyway.
foreach (self::getEnvConfig('load-libraries') as $library) {
phutil_load_library($library);
}
// Drop any class map caches, since they will have generated without
// any classes from libraries. Without this, preflight setup checks can
// cause generation of a setup check cache that omits checks defined in
// libraries, for example.
PhutilClassMapQuery::deleteCaches();
// If custom libraries specify config options, they won't get default
// values as the Default source has already been loaded, so we get it to
// pull in all options from non-phabricator libraries now they are loaded.
$default_source->loadExternalOptions();
// If this install has site config sources, load them now.
$site_sources = id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorConfigSiteSource')
->setSortMethod('getPriority')
->execute();
foreach ($site_sources as $site_source) {
$stack->pushSource($site_source);
// If the site source did anything which reads config, throw it away
// to make sure any additional site sources get clean reads.
self::dropConfigCache();
}
$masters = PhabricatorDatabaseRef::getMasterDatabaseRefs();
if (!$masters) {
self::setReadOnly(true, self::READONLY_MASTERLESS);
} else {
// If any master is severed, we drop to readonly mode. In theory we
// could try to continue if we're only missing some applications, but
// this is very complex and we're unlikely to get it right.
foreach ($masters as $master) {
// Give severed masters one last chance to get healthy.
if ($master->isSevered()) {
$master->checkHealth();
}
if ($master->isSevered()) {
self::setReadOnly(true, self::READONLY_SEVERED);
break;
}
}
}
try {
$stack->pushSource(
id(new PhabricatorConfigDatabaseSource('default'))
->setName(pht('Database')));
} catch (AphrontSchemaQueryException $exception) {
// If the database is not available, just skip this configuration
// source. This happens during `bin/storage upgrade`, `bin/conf` before
// schema setup, etc.
} catch (PhabricatorClusterStrandedException $ex) {
// This means we can't connect to any database host. That's fine as
// long as we're running a setup script like `bin/storage`.
if (!$config_optional) {
throw $ex;
}
}
// Drop the config cache one final time to make sure we're getting clean
// reads now that we've finished building the stack.
self::dropConfigCache();
}
public static function repairConfig($key, $value) {
if (!self::$repairSource) {
self::$repairSource = id(new PhabricatorConfigDictionarySource(array()))
->setName(pht('Repaired Config'));
self::$sourceStack->pushSource(self::$repairSource);
}
self::$repairSource->setKeys(array($key => $value));
self::dropConfigCache();
}
public static function overrideConfig($key, $value) {
if (!self::$overrideSource) {
self::$overrideSource = id(new PhabricatorConfigDictionarySource(array()))
->setName(pht('Overridden Config'));
self::$sourceStack->pushSource(self::$overrideSource);
}
self::$overrideSource->setKeys(array($key => $value));
self::dropConfigCache();
}
public static function getUnrepairedEnvConfig($key, $default = null) {
foreach (self::$sourceStack->getStack() as $source) {
if ($source === self::$repairSource) {
continue;
}
$result = $source->getKeys(array($key));
if ($result) {
return $result[$key];
}
}
return $default;
}
public static function getSelectedEnvironmentName() {
$env_var = 'PHABRICATOR_ENV';
$env = idx($_SERVER, $env_var);
if (!$env) {
$env = getenv($env_var);
}
if (!$env) {
$env = idx($_ENV, $env_var);
}
if (!$env) {
$root = dirname(phutil_get_library_root('phabricator'));
$path = $root.'/conf/local/ENVIRONMENT';
if (Filesystem::pathExists($path)) {
$env = trim(Filesystem::readFile($path));
}
}
return $env;
}
/* -( Reading Configuration )---------------------------------------------- */
/**
* Get the current configuration setting for a given key.
*
* If the key is not found, then throw an Exception.
*
* @task read
*/
public static function getEnvConfig($key) {
if (!self::$sourceStack) {
throw new Exception(
pht(
'Trying to read configuration "%s" before configuration has been '.
'initialized.',
$key));
}
if (isset(self::$cache[$key])) {
return self::$cache[$key];
}
if (array_key_exists($key, self::$cache)) {
return self::$cache[$key];
}
if (!self::$sourceStack) {
throw new Exception(
pht(
'Trying to read configuration "%s" before configuration has been '.
'initialized.',
$key));
}
$result = self::$sourceStack->getKeys(array($key));
if (array_key_exists($key, $result)) {
self::$cache[$key] = $result[$key];
return $result[$key];
} else {
throw new Exception(
pht(
"No config value specified for key '%s'.",
$key));
}
}
/**
* Get the current configuration setting for a given key. If the key
* does not exist, return a default value instead of throwing. This is
* primarily useful for migrations involving keys which are slated for
* removal.
*
* @task read
*/
public static function getEnvConfigIfExists($key, $default = null) {
try {
return self::getEnvConfig($key);
} catch (Exception $ex) {
return $default;
}
}
/**
* Get the fully-qualified URI for a path.
*
* @task read
*/
public static function getURI($path) {
return rtrim(self::getAnyBaseURI(), '/').$path;
}
/**
* Get the fully-qualified production URI for a path.
*
* @task read
*/
public static function getProductionURI($path) {
// If we're passed a URI which already has a domain, simply return it
// unmodified. In particular, files may have URIs which point to a CDN
// domain.
$uri = new PhutilURI($path);
if ($uri->getDomain()) {
return $path;
}
$production_domain = self::getEnvConfig('phabricator.production-uri');
if (!$production_domain) {
$production_domain = self::getAnyBaseURI();
}
return rtrim($production_domain, '/').$path;
}
public static function isSelfURI($raw_uri) {
$uri = new PhutilURI($raw_uri);
$host = $uri->getDomain();
if (!strlen($host)) {
return false;
}
$host = phutil_utf8_strtolower($host);
$self_map = self::getSelfURIMap();
return isset($self_map[$host]);
}
private static function getSelfURIMap() {
$self_uris = array();
$self_uris[] = self::getProductionURI('/');
$self_uris[] = self::getURI('/');
$allowed_uris = self::getEnvConfig('phabricator.allowed-uris');
foreach ($allowed_uris as $allowed_uri) {
$self_uris[] = $allowed_uri;
}
$self_map = array();
foreach ($self_uris as $self_uri) {
$host = id(new PhutilURI($self_uri))->getDomain();
if (!strlen($host)) {
continue;
}
$host = phutil_utf8_strtolower($host);
$self_map[$host] = $host;
}
return $self_map;
}
/**
* Get the fully-qualified production URI for a static resource path.
*
* @task read
*/
public static function getCDNURI($path) {
$alt = self::getEnvConfig('security.alternate-file-domain');
if (!$alt) {
$alt = self::getAnyBaseURI();
}
$uri = new PhutilURI($alt);
$uri->setPath($path);
return (string)$uri;
}
/**
* Get the fully-qualified production URI for a documentation resource.
*
* @task read
*/
public static function getDoclink($resource, $type = 'article') {
- $uri = new PhutilURI('https://secure.phabricator.com/diviner/find/');
- $uri->setQueryParam('name', $resource);
- $uri->setQueryParam('type', $type);
- $uri->setQueryParam('jump', true);
- return (string)$uri;
+ $params = array(
+ 'name' => $resource,
+ 'type' => $type,
+ 'jump' => true,
+ );
+
+ $uri = new PhutilURI(
+ 'https://secure.phabricator.com/diviner/find/',
+ $params);
+
+ return phutil_string_cast($uri);
}
/**
* Build a concrete object from a configuration key.
*
* @task read
*/
public static function newObjectFromConfig($key, $args = array()) {
$class = self::getEnvConfig($key);
return newv($class, $args);
}
public static function getAnyBaseURI() {
$base_uri = self::getEnvConfig('phabricator.base-uri');
if (!$base_uri) {
$base_uri = self::getRequestBaseURI();
}
if (!$base_uri) {
throw new Exception(
pht(
"Define '%s' in your configuration to continue.",
'phabricator.base-uri'));
}
return $base_uri;
}
public static function getRequestBaseURI() {
return self::$requestBaseURI;
}
public static function setRequestBaseURI($uri) {
self::$requestBaseURI = $uri;
}
public static function isReadOnly() {
if (self::$readOnly !== null) {
return self::$readOnly;
}
return self::getEnvConfig('cluster.read-only');
}
public static function setReadOnly($read_only, $reason) {
self::$readOnly = $read_only;
self::$readOnlyReason = $reason;
}
public static function getReadOnlyMessage() {
$reason = self::getReadOnlyReason();
switch ($reason) {
case self::READONLY_MASTERLESS:
return pht(
'Phabricator is in read-only mode (no writable database '.
'is configured).');
case self::READONLY_UNREACHABLE:
return pht(
'Phabricator is in read-only mode (unreachable master).');
case self::READONLY_SEVERED:
return pht(
'Phabricator is in read-only mode (major interruption).');
}
return pht('Phabricator is in read-only mode.');
}
public static function getReadOnlyURI() {
return urisprintf(
'/readonly/%s/',
self::getReadOnlyReason());
}
public static function getReadOnlyReason() {
if (!self::isReadOnly()) {
return null;
}
if (self::$readOnlyReason !== null) {
return self::$readOnlyReason;
}
return self::READONLY_CONFIG;
}
/* -( Unit Test Support )-------------------------------------------------- */
/**
* @task test
*/
public static function beginScopedEnv() {
return new PhabricatorScopedEnv(self::pushTestEnvironment());
}
/**
* @task test
*/
private static function pushTestEnvironment() {
self::dropConfigCache();
$source = new PhabricatorConfigDictionarySource(array());
self::$sourceStack->pushSource($source);
return spl_object_hash($source);
}
/**
* @task test
*/
public static function popTestEnvironment($key) {
self::dropConfigCache();
$source = self::$sourceStack->popSource();
$stack_key = spl_object_hash($source);
if ($stack_key !== $key) {
self::$sourceStack->pushSource($source);
throw new Exception(
pht(
'Scoped environments were destroyed in a different order than they '.
'were initialized.'));
}
}
/* -( URI Validation )----------------------------------------------------- */
/**
* Detect if a URI satisfies either @{method:isValidLocalURIForLink} or
* @{method:isValidRemoteURIForLink}, i.e. is a page on this server or the
* URI of some other resource which has a valid protocol. This rejects
* garbage URIs and URIs with protocols which do not appear in the
* `uri.allowed-protocols` configuration, notably 'javascript:' URIs.
*
* NOTE: This method is generally intended to reject URIs which it may be
* unsafe to put in an "href" link attribute.
*
* @param string URI to test.
* @return bool True if the URI identifies a web resource.
* @task uri
*/
public static function isValidURIForLink($uri) {
return self::isValidLocalURIForLink($uri) ||
self::isValidRemoteURIForLink($uri);
}
/**
* Detect if a URI identifies some page on this server.
*
* NOTE: This method is generally intended to reject URIs which it may be
* unsafe to issue a "Location:" redirect to.
*
* @param string URI to test.
* @return bool True if the URI identifies a local page.
* @task uri
*/
public static function isValidLocalURIForLink($uri) {
$uri = (string)$uri;
if (!strlen($uri)) {
return false;
}
if (preg_match('/\s/', $uri)) {
// PHP hasn't been vulnerable to header injection attacks for a bunch of
// years, but we can safely reject these anyway since they're never valid.
return false;
}
// Chrome (at a minimum) interprets backslashes in Location headers and the
// URL bar as forward slashes. This is probably intended to reduce user
// error caused by confusion over which key is "forward slash" vs "back
// slash".
//
// However, it means a URI like "/\evil.com" is interpreted like
// "//evil.com", which is a protocol relative remote URI.
//
// Since we currently never generate URIs with backslashes in them, reject
// these unconditionally rather than trying to figure out how browsers will
// interpret them.
if (preg_match('/\\\\/', $uri)) {
return false;
}
// Valid URIs must begin with '/', followed by the end of the string or some
// other non-'/' character. This rejects protocol-relative URIs like
// "//evil.com/evil_stuff/".
return (bool)preg_match('@^/([^/]|$)@', $uri);
}
/**
* Detect if a URI identifies some valid linkable remote resource.
*
* @param string URI to test.
* @return bool True if a URI identifies a remote resource with an allowed
* protocol.
* @task uri
*/
public static function isValidRemoteURIForLink($uri) {
try {
self::requireValidRemoteURIForLink($uri);
return true;
} catch (Exception $ex) {
return false;
}
}
/**
* Detect if a URI identifies a valid linkable remote resource, throwing a
* detailed message if it does not.
*
* A valid linkable remote resource can be safely linked or redirected to.
* This is primarily a protocol whitelist check.
*
* @param string URI to test.
* @return void
* @task uri
*/
public static function requireValidRemoteURIForLink($raw_uri) {
$uri = new PhutilURI($raw_uri);
$proto = $uri->getProtocol();
if (!strlen($proto)) {
throw new Exception(
pht(
'URI "%s" is not a valid linkable resource. A valid linkable '.
'resource URI must specify a protocol.',
$raw_uri));
}
$protocols = self::getEnvConfig('uri.allowed-protocols');
if (!isset($protocols[$proto])) {
throw new Exception(
pht(
'URI "%s" is not a valid linkable resource. A valid linkable '.
'resource URI must use one of these protocols: %s.',
$raw_uri,
implode(', ', array_keys($protocols))));
}
$domain = $uri->getDomain();
if (!strlen($domain)) {
throw new Exception(
pht(
'URI "%s" is not a valid linkable resource. A valid linkable '.
'resource URI must specify a domain.',
$raw_uri));
}
}
/**
* Detect if a URI identifies a valid fetchable remote resource.
*
* @param string URI to test.
* @param list<string> Allowed protocols.
* @return bool True if the URI is a valid fetchable remote resource.
* @task uri
*/
public static function isValidRemoteURIForFetch($uri, array $protocols) {
try {
self::requireValidRemoteURIForFetch($uri, $protocols);
return true;
} catch (Exception $ex) {
return false;
}
}
/**
* Detect if a URI identifies a valid fetchable remote resource, throwing
* a detailed message if it does not.
*
* A valid fetchable remote resource can be safely fetched using a request
* originating on this server. This is a primarily an address check against
* the outbound address blacklist.
*
* @param string URI to test.
* @param list<string> Allowed protocols.
* @return pair<string, string> Pre-resolved URI and domain.
* @task uri
*/
public static function requireValidRemoteURIForFetch(
$raw_uri,
array $protocols) {
$uri = new PhutilURI($raw_uri);
$proto = $uri->getProtocol();
if (!strlen($proto)) {
throw new Exception(
pht(
'URI "%s" is not a valid fetchable resource. A valid fetchable '.
'resource URI must specify a protocol.',
$raw_uri));
}
$protocols = array_fuse($protocols);
if (!isset($protocols[$proto])) {
throw new Exception(
pht(
'URI "%s" is not a valid fetchable resource. A valid fetchable '.
'resource URI must use one of these protocols: %s.',
$raw_uri,
implode(', ', array_keys($protocols))));
}
$domain = $uri->getDomain();
if (!strlen($domain)) {
throw new Exception(
pht(
'URI "%s" is not a valid fetchable resource. A valid fetchable '.
'resource URI must specify a domain.',
$raw_uri));
}
$addresses = gethostbynamel($domain);
if (!$addresses) {
throw new Exception(
pht(
'URI "%s" is not a valid fetchable resource. The domain "%s" could '.
'not be resolved.',
$raw_uri,
$domain));
}
foreach ($addresses as $address) {
if (self::isBlacklistedOutboundAddress($address)) {
throw new Exception(
pht(
'URI "%s" is not a valid fetchable resource. The domain "%s" '.
'resolves to the address "%s", which is blacklisted for '.
'outbound requests.',
$raw_uri,
$domain,
$address));
}
}
$resolved_uri = clone $uri;
$resolved_uri->setDomain(head($addresses));
return array($resolved_uri, $domain);
}
/**
* Determine if an IP address is in the outbound address blacklist.
*
* @param string IP address.
* @return bool True if the address is blacklisted.
*/
public static function isBlacklistedOutboundAddress($address) {
$blacklist = self::getEnvConfig('security.outbound-blacklist');
return PhutilCIDRList::newList($blacklist)->containsAddress($address);
}
public static function isClusterRemoteAddress() {
$cluster_addresses = self::getEnvConfig('cluster.addresses');
if (!$cluster_addresses) {
return false;
}
$address = self::getRemoteAddress();
if (!$address) {
throw new Exception(
pht(
'Unable to test remote address against cluster whitelist: '.
'REMOTE_ADDR is not defined or not valid.'));
}
return self::isClusterAddress($address);
}
public static function isClusterAddress($address) {
$cluster_addresses = self::getEnvConfig('cluster.addresses');
if (!$cluster_addresses) {
throw new Exception(
pht(
'Phabricator is not configured to serve cluster requests. '.
'Set `cluster.addresses` in the configuration to whitelist '.
'cluster hosts before sending requests that use a cluster '.
'authentication mechanism.'));
}
return PhutilCIDRList::newList($cluster_addresses)
->containsAddress($address);
}
public static function getRemoteAddress() {
$address = idx($_SERVER, 'REMOTE_ADDR');
if (!$address) {
return null;
}
try {
return PhutilIPAddress::newAddress($address);
} catch (Exception $ex) {
return null;
}
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
public static function envConfigExists($key) {
return array_key_exists($key, self::$sourceStack->getKeys(array($key)));
}
/**
* @task internal
*/
public static function getAllConfigKeys() {
return self::$sourceStack->getAllKeys();
}
public static function getConfigSourceStack() {
return self::$sourceStack;
}
/**
* @task internal
*/
public static function overrideTestEnvConfig($stack_key, $key, $value) {
$tmp = array();
// If we don't have the right key, we'll throw when popping the last
// source off the stack.
do {
$source = self::$sourceStack->popSource();
array_unshift($tmp, $source);
if (spl_object_hash($source) == $stack_key) {
$source->setKeys(array($key => $value));
break;
}
} while (true);
foreach ($tmp as $source) {
self::$sourceStack->pushSource($source);
}
self::dropConfigCache();
}
private static function dropConfigCache() {
self::$cache = array();
}
private static function resetUmask() {
// Reset the umask to the common standard umask. The umask controls default
// permissions when files are created and propagates to subprocesses.
// "022" is the most common umask, but sometimes it is set to something
// unusual by the calling environment.
// Since various things rely on this umask to work properly and we are
// not aware of any legitimate reasons to adjust it, unconditionally
// normalize it until such reasons arise. See T7475 for discussion.
umask(022);
}
/**
* Get the path to an empty directory which is readable by all of the system
* user accounts that Phabricator acts as.
*
* In some cases, a binary needs some valid HOME or CWD to continue, but not
* all user accounts have valid home directories and even if they do they
* may not be readable after a `sudo` operation.
*
* @return string Path to an empty directory suitable for use as a CWD.
*/
public static function getEmptyCWD() {
$root = dirname(phutil_get_library_root('phabricator'));
return $root.'/support/empty/';
}
}
diff --git a/src/infrastructure/export/field/PhabricatorOptionExportField.php b/src/infrastructure/export/field/PhabricatorOptionExportField.php
new file mode 100644
index 000000000..e6d3e9b45
--- /dev/null
+++ b/src/infrastructure/export/field/PhabricatorOptionExportField.php
@@ -0,0 +1,47 @@
+<?php
+
+final class PhabricatorOptionExportField
+ extends PhabricatorExportField {
+
+ private $options;
+
+ public function setOptions(array $options) {
+ $this->options = $options;
+ return $this;
+ }
+
+ public function getOptions() {
+ return $this->options;
+ }
+
+ public function getNaturalValue($value) {
+ if ($value === null) {
+ return $value;
+ }
+
+ if (!strlen($value)) {
+ return null;
+ }
+
+ $options = $this->getOptions();
+
+ return array(
+ 'value' => (string)$value,
+ 'name' => (string)idx($options, $value, $value),
+ );
+ }
+
+ public function getTextValue($value) {
+ $natural_value = $this->getNaturalValue($value);
+ if ($natural_value === null) {
+ return null;
+ }
+
+ return $natural_value['name'];
+ }
+
+ public function getPHPExcelValue($value) {
+ return $this->getTextValue($value);
+ }
+
+}
diff --git a/src/infrastructure/graph/ManiphestTaskGraph.php b/src/infrastructure/graph/ManiphestTaskGraph.php
index 99191760d..74a1fe870 100644
--- a/src/infrastructure/graph/ManiphestTaskGraph.php
+++ b/src/infrastructure/graph/ManiphestTaskGraph.php
@@ -1,170 +1,188 @@
<?php
final class ManiphestTaskGraph
extends PhabricatorObjectGraph {
private $seedMaps = array();
+ private $isStandalone;
protected function getEdgeTypes() {
return array(
ManiphestTaskDependedOnByTaskEdgeType::EDGECONST,
ManiphestTaskDependsOnTaskEdgeType::EDGECONST,
);
}
protected function getParentEdgeType() {
return ManiphestTaskDependsOnTaskEdgeType::EDGECONST;
}
protected function newQuery() {
return new ManiphestTaskQuery();
}
protected function isClosed($object) {
return $object->isClosed();
}
+ public function setIsStandalone($is_standalone) {
+ $this->isStandalone = $is_standalone;
+ return $this;
+ }
+
+ public function getIsStandalone() {
+ return $this->isStandalone;
+ }
+
protected function newTableRow($phid, $object, $trace) {
$viewer = $this->getViewer();
Javelin::initBehavior('phui-hovercards');
if ($object) {
$status = $object->getStatus();
$priority = $object->getPriority();
$status_icon = ManiphestTaskStatus::getStatusIcon($status);
$status_name = ManiphestTaskStatus::getTaskStatusName($status);
$priority_color = ManiphestTaskPriority::getTaskPriorityColor($priority);
if ($object->isClosed()) {
$priority_color = 'grey';
}
$status = array(
id(new PHUIIconView())->setIcon($status_icon, $priority_color),
' ',
$status_name,
);
$owner_phid = $object->getOwnerPHID();
if ($owner_phid) {
$assigned = $viewer->renderHandle($owner_phid);
} else {
$assigned = phutil_tag('em', array(), pht('None'));
}
$link = javelin_tag(
'a',
array(
'href' => $object->getURI(),
'sigil' => 'hovercard',
'meta' => array(
'hoverPHID' => $object->getPHID(),
),
),
$object->getTitle());
$link = array(
phutil_tag(
'span',
array(
'class' => 'object-name',
),
$object->getMonogram()),
' ',
$link,
);
} else {
$status = null;
$assigned = null;
$link = $viewer->renderHandle($phid);
}
if ($this->isParentTask($phid)) {
$marker = 'fa-chevron-circle-up bluegrey';
$marker_tip = pht('Direct Parent');
} else if ($this->isChildTask($phid)) {
$marker = 'fa-chevron-circle-down bluegrey';
$marker_tip = pht('Direct Subtask');
} else {
$marker = null;
}
if ($marker) {
$marker = id(new PHUIIconView())
->setIcon($marker)
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => $marker_tip,
'align' => 'E',
));
}
return array(
$marker,
$trace,
$status,
$assigned,
$link,
);
}
protected function newTable(AphrontTableView $table) {
return $table
->setHeaders(
array(
null,
null,
pht('Status'),
pht('Assigned'),
pht('Task'),
))
->setColumnClasses(
array(
'nudgeright',
'threads',
'graph-status',
null,
'wide pri object-link',
))
->setColumnVisibility(
array(
true,
!$this->getRenderOnlyAdjacentNodes(),
+ ))
+ ->setDeviceVisibility(
+ array(
+ true,
+
+ // On mobile, we only show the actual graph drawing if we're on the
+ // standalone page, since it can take over the screen otherwise.
+ $this->getIsStandalone(),
));
}
private function isParentTask($task_phid) {
$map = $this->getSeedMap(ManiphestTaskDependedOnByTaskEdgeType::EDGECONST);
return isset($map[$task_phid]);
}
private function isChildTask($task_phid) {
$map = $this->getSeedMap(ManiphestTaskDependsOnTaskEdgeType::EDGECONST);
return isset($map[$task_phid]);
}
private function getSeedMap($type) {
if (!isset($this->seedMaps[$type])) {
$maps = $this->getEdges($type);
$phids = idx($maps, $this->getSeedPHID(), array());
$phids = array_fuse($phids);
$this->seedMaps[$type] = $phids;
}
return $this->seedMaps[$type];
}
protected function newEllipsisRow() {
return array(
null,
null,
null,
null,
pht("\xC2\xB7 \xC2\xB7 \xC2\xB7"),
);
}
}
diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php
index 868bbc567..3a63cb97a 100644
--- a/src/infrastructure/markup/PhabricatorMarkupEngine.php
+++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php
@@ -1,713 +1,713 @@
<?php
/**
* Manages markup engine selection, configuration, application, caching and
* pipelining.
*
* @{class:PhabricatorMarkupEngine} can be used to render objects which
* implement @{interface:PhabricatorMarkupInterface} in a batched, cache-aware
* way. For example, if you have a list of comments written in remarkup (and
* the objects implement the correct interface) you can render them by first
* building an engine and adding the fields with @{method:addObject}.
*
* $field = 'field:body'; // Field you want to render. Each object exposes
* // one or more fields of markup.
*
* $engine = new PhabricatorMarkupEngine();
* foreach ($comments as $comment) {
* $engine->addObject($comment, $field);
* }
*
* Now, call @{method:process} to perform the actual cache/rendering
* step. This is a heavyweight call which does batched data access and
* transforms the markup into output.
*
* $engine->process();
*
* Finally, do something with the results:
*
* $results = array();
* foreach ($comments as $comment) {
* $results[] = $engine->getOutput($comment, $field);
* }
*
* If you have a single object to render, you can use the convenience method
* @{method:renderOneObject}.
*
* @task markup Markup Pipeline
* @task engine Engine Construction
*/
final class PhabricatorMarkupEngine extends Phobject {
private $objects = array();
private $viewer;
private $contextObject;
- private $version = 17;
+ private $version = 18;
private $engineCaches = array();
private $auxiliaryConfig = array();
/* -( Markup Pipeline )---------------------------------------------------- */
/**
* Convenience method for pushing a single object through the markup
* pipeline.
*
* @param PhabricatorMarkupInterface The object to render.
* @param string The field to render.
* @param PhabricatorUser User viewing the markup.
* @param object A context object for policy checks
* @return string Marked up output.
* @task markup
*/
public static function renderOneObject(
PhabricatorMarkupInterface $object,
$field,
PhabricatorUser $viewer,
$context_object = null) {
return id(new PhabricatorMarkupEngine())
->setViewer($viewer)
->setContextObject($context_object)
->addObject($object, $field)
->process()
->getOutput($object, $field);
}
/**
* Queue an object for markup generation when @{method:process} is
* called. You can retrieve the output later with @{method:getOutput}.
*
* @param PhabricatorMarkupInterface The object to render.
* @param string The field to render.
* @return this
* @task markup
*/
public function addObject(PhabricatorMarkupInterface $object, $field) {
$key = $this->getMarkupFieldKey($object, $field);
$this->objects[$key] = array(
'object' => $object,
'field' => $field,
);
return $this;
}
/**
* Process objects queued with @{method:addObject}. You can then retrieve
* the output with @{method:getOutput}.
*
* @return this
* @task markup
*/
public function process() {
$keys = array();
foreach ($this->objects as $key => $info) {
if (!isset($info['markup'])) {
$keys[] = $key;
}
}
if (!$keys) {
return $this;
}
$objects = array_select_keys($this->objects, $keys);
// Build all the markup engines. We need an engine for each field whether
// we have a cache or not, since we still need to postprocess the cache.
$engines = array();
foreach ($objects as $key => $info) {
$engines[$key] = $info['object']->newMarkupEngine($info['field']);
$engines[$key]->setConfig('viewer', $this->viewer);
$engines[$key]->setConfig('contextObject', $this->contextObject);
foreach ($this->auxiliaryConfig as $aux_key => $aux_value) {
$engines[$key]->setConfig($aux_key, $aux_value);
}
}
// Load or build the preprocessor caches.
$blocks = $this->loadPreprocessorCaches($engines, $objects);
$blocks = mpull($blocks, 'getCacheData');
$this->engineCaches = $blocks;
// Finalize the output.
foreach ($objects as $key => $info) {
$engine = $engines[$key];
$field = $info['field'];
$object = $info['object'];
$output = $engine->postprocessText($blocks[$key]);
$output = $object->didMarkupText($field, $output, $engine);
$this->objects[$key]['output'] = $output;
}
return $this;
}
/**
* Get the output of markup processing for a field queued with
* @{method:addObject}. Before you can call this method, you must call
* @{method:process}.
*
* @param PhabricatorMarkupInterface The object to retrieve.
* @param string The field to retrieve.
* @return string Processed output.
* @task markup
*/
public function getOutput(PhabricatorMarkupInterface $object, $field) {
$key = $this->getMarkupFieldKey($object, $field);
$this->requireKeyProcessed($key);
return $this->objects[$key]['output'];
}
/**
* Retrieve engine metadata for a given field.
*
* @param PhabricatorMarkupInterface The object to retrieve.
* @param string The field to retrieve.
* @param string The engine metadata field to retrieve.
* @param wild Optional default value.
* @task markup
*/
public function getEngineMetadata(
PhabricatorMarkupInterface $object,
$field,
$metadata_key,
$default = null) {
$key = $this->getMarkupFieldKey($object, $field);
$this->requireKeyProcessed($key);
return idx($this->engineCaches[$key]['metadata'], $metadata_key, $default);
}
/**
* @task markup
*/
private function requireKeyProcessed($key) {
if (empty($this->objects[$key])) {
throw new Exception(
pht(
"Call %s before using results (key = '%s').",
'addObject()',
$key));
}
if (!isset($this->objects[$key]['output'])) {
throw new PhutilInvalidStateException('process');
}
}
/**
* @task markup
*/
private function getMarkupFieldKey(
PhabricatorMarkupInterface $object,
$field) {
static $custom;
if ($custom === null) {
$custom = array_merge(
self::loadCustomInlineRules(),
self::loadCustomBlockRules());
$custom = mpull($custom, 'getRuleVersion', null);
ksort($custom);
$custom = PhabricatorHash::digestForIndex(serialize($custom));
}
return $object->getMarkupFieldKey($field).'@'.$this->version.'@'.$custom;
}
/**
* @task markup
*/
private function loadPreprocessorCaches(array $engines, array $objects) {
$blocks = array();
$use_cache = array();
foreach ($objects as $key => $info) {
if ($info['object']->shouldUseMarkupCache($info['field'])) {
$use_cache[$key] = true;
}
}
if ($use_cache) {
try {
$blocks = id(new PhabricatorMarkupCache())->loadAllWhere(
'cacheKey IN (%Ls)',
array_keys($use_cache));
$blocks = mpull($blocks, null, 'getCacheKey');
} catch (Exception $ex) {
phlog($ex);
}
}
$is_readonly = PhabricatorEnv::isReadOnly();
foreach ($objects as $key => $info) {
// False check in case MySQL doesn't support unicode characters
// in the string (T1191), resulting in unserialize returning false.
if (isset($blocks[$key]) && $blocks[$key]->getCacheData() !== false) {
// If we already have a preprocessing cache, we don't need to rebuild
// it.
continue;
}
$text = $info['object']->getMarkupText($info['field']);
$data = $engines[$key]->preprocessText($text);
// NOTE: This is just debugging information to help sort out cache issues.
// If one machine is misconfigured and poisoning caches you can use this
// field to hunt it down.
$metadata = array(
'host' => php_uname('n'),
);
$blocks[$key] = id(new PhabricatorMarkupCache())
->setCacheKey($key)
->setCacheData($data)
->setMetadata($metadata);
if (isset($use_cache[$key]) && !$is_readonly) {
// This is just filling a cache and always safe, even on a read pathway.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$blocks[$key]->replace();
unset($unguarded);
}
}
return $blocks;
}
/**
* Set the viewing user. Used to implement object permissions.
*
* @param PhabricatorUser The viewing user.
* @return this
* @task markup
*/
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
/**
* Set the context object. Used to implement object permissions.
*
* @param The object in which context this remarkup is used.
* @return this
* @task markup
*/
public function setContextObject($object) {
$this->contextObject = $object;
return $this;
}
public function setAuxiliaryConfig($key, $value) {
// TODO: This is gross and should be removed. Avoid use.
$this->auxiliaryConfig[$key] = $value;
return $this;
}
/* -( Engine Construction )------------------------------------------------ */
/**
* @task engine
*/
public static function newManiphestMarkupEngine() {
return self::newMarkupEngine(array(
));
}
/**
* @task engine
*/
public static function newPhrictionMarkupEngine() {
return self::newMarkupEngine(array(
'header.generate-toc' => true,
));
}
/**
* @task engine
*/
public static function newPhameMarkupEngine() {
return self::newMarkupEngine(
array(
'macros' => false,
'uri.full' => true,
'uri.same-window' => true,
'uri.base' => PhabricatorEnv::getURI('/'),
));
}
/**
* @task engine
*/
public static function newFeedMarkupEngine() {
return self::newMarkupEngine(
array(
'macros' => false,
'youtube' => false,
));
}
/**
* @task engine
*/
public static function newCalendarMarkupEngine() {
return self::newMarkupEngine(array(
));
}
/**
* @task engine
*/
public static function newDifferentialMarkupEngine(array $options = array()) {
return self::newMarkupEngine(array(
'differential.diff' => idx($options, 'differential.diff'),
));
}
/**
* @task engine
*/
public static function newDiffusionMarkupEngine(array $options = array()) {
return self::newMarkupEngine(array(
'header.generate-toc' => true,
));
}
/**
* @task engine
*/
public static function getEngine($ruleset = 'default') {
static $engines = array();
if (isset($engines[$ruleset])) {
return $engines[$ruleset];
}
$engine = null;
switch ($ruleset) {
case 'default':
$engine = self::newMarkupEngine(array());
break;
case 'feed':
$engine = self::newMarkupEngine(array());
$engine->setConfig('autoplay.disable', true);
break;
case 'nolinebreaks':
$engine = self::newMarkupEngine(array());
$engine->setConfig('preserve-linebreaks', false);
break;
case 'diffusion-readme':
$engine = self::newMarkupEngine(array());
$engine->setConfig('preserve-linebreaks', false);
$engine->setConfig('header.generate-toc', true);
break;
case 'diviner':
$engine = self::newMarkupEngine(array());
$engine->setConfig('preserve-linebreaks', false);
// $engine->setConfig('diviner.renderer', new DivinerDefaultRenderer());
$engine->setConfig('header.generate-toc', true);
break;
case 'extract':
// Engine used for reference/edge extraction. Turn off anything which
// is slow and doesn't change reference extraction.
$engine = self::newMarkupEngine(array());
$engine->setConfig('pygments.enabled', false);
break;
default:
throw new Exception(pht('Unknown engine ruleset: %s!', $ruleset));
}
$engines[$ruleset] = $engine;
return $engine;
}
/**
* @task engine
*/
private static function getMarkupEngineDefaultConfiguration() {
return array(
'pygments' => PhabricatorEnv::getEnvConfig('pygments.enabled'),
'youtube' => PhabricatorEnv::getEnvConfig(
'remarkup.enable-embedded-youtube'),
'differential.diff' => null,
'header.generate-toc' => false,
'macros' => true,
'uri.allowed-protocols' => PhabricatorEnv::getEnvConfig(
'uri.allowed-protocols'),
'uri.full' => false,
'syntax-highlighter.engine' => PhabricatorEnv::getEnvConfig(
'syntax-highlighter.engine'),
'preserve-linebreaks' => true,
);
}
/**
* @task engine
*/
public static function newMarkupEngine(array $options) {
$options += self::getMarkupEngineDefaultConfiguration();
$engine = new PhutilRemarkupEngine();
$engine->setConfig('preserve-linebreaks', $options['preserve-linebreaks']);
$engine->setConfig('pygments.enabled', $options['pygments']);
$engine->setConfig(
'uri.allowed-protocols',
$options['uri.allowed-protocols']);
$engine->setConfig('differential.diff', $options['differential.diff']);
$engine->setConfig('header.generate-toc', $options['header.generate-toc']);
$engine->setConfig(
'syntax-highlighter.engine',
$options['syntax-highlighter.engine']);
$style_map = id(new PhabricatorDefaultSyntaxStyle())
->getRemarkupStyleMap();
$engine->setConfig('phutil.codeblock.style-map', $style_map);
$engine->setConfig('uri.full', $options['uri.full']);
if (isset($options['uri.base'])) {
$engine->setConfig('uri.base', $options['uri.base']);
}
if (isset($options['uri.same-window'])) {
$engine->setConfig('uri.same-window', $options['uri.same-window']);
}
$rules = array();
$rules[] = new PhutilRemarkupEscapeRemarkupRule();
$rules[] = new PhutilRemarkupMonospaceRule();
$rules[] = new PhutilRemarkupDocumentLinkRule();
$rules[] = new PhabricatorNavigationRemarkupRule();
$rules[] = new PhabricatorKeyboardRemarkupRule();
$rules[] = new PhabricatorConfigRemarkupRule();
if ($options['youtube']) {
$rules[] = new PhabricatorYoutubeRemarkupRule();
}
$rules[] = new PhabricatorIconRemarkupRule();
$rules[] = new PhabricatorEmojiRemarkupRule();
$rules[] = new PhabricatorHandleRemarkupRule();
$applications = PhabricatorApplication::getAllInstalledApplications();
foreach ($applications as $application) {
foreach ($application->getRemarkupRules() as $rule) {
$rules[] = $rule;
}
}
$rules[] = new PhutilRemarkupHyperlinkRule();
if ($options['macros']) {
$rules[] = new PhabricatorImageMacroRemarkupRule();
$rules[] = new PhabricatorMemeRemarkupRule();
}
$rules[] = new PhutilRemarkupBoldRule();
$rules[] = new PhutilRemarkupItalicRule();
$rules[] = new PhutilRemarkupDelRule();
$rules[] = new PhutilRemarkupUnderlineRule();
$rules[] = new PhutilRemarkupHighlightRule();
foreach (self::loadCustomInlineRules() as $rule) {
$rules[] = clone $rule;
}
$blocks = array();
$blocks[] = new PhutilRemarkupQuotesBlockRule();
$blocks[] = new PhutilRemarkupReplyBlockRule();
$blocks[] = new PhutilRemarkupLiteralBlockRule();
$blocks[] = new PhutilRemarkupHeaderBlockRule();
$blocks[] = new PhutilRemarkupHorizontalRuleBlockRule();
$blocks[] = new PhutilRemarkupListBlockRule();
$blocks[] = new PhutilRemarkupCodeBlockRule();
$blocks[] = new PhutilRemarkupNoteBlockRule();
$blocks[] = new PhutilRemarkupTableBlockRule();
$blocks[] = new PhutilRemarkupSimpleTableBlockRule();
$blocks[] = new PhutilRemarkupInterpreterBlockRule();
$blocks[] = new PhutilRemarkupDefaultBlockRule();
foreach (self::loadCustomBlockRules() as $rule) {
$blocks[] = $rule;
}
foreach ($blocks as $block) {
$block->setMarkupRules($rules);
}
$engine->setBlockRules($blocks);
return $engine;
}
public static function extractPHIDsFromMentions(
PhabricatorUser $viewer,
array $content_blocks) {
$mentions = array();
$engine = self::newDifferentialMarkupEngine();
$engine->setConfig('viewer', $viewer);
foreach ($content_blocks as $content_block) {
$engine->markupText($content_block);
$phids = $engine->getTextMetadata(
PhabricatorMentionRemarkupRule::KEY_MENTIONED,
array());
$mentions += $phids;
}
return $mentions;
}
public static function extractFilePHIDsFromEmbeddedFiles(
PhabricatorUser $viewer,
array $content_blocks) {
$files = array();
$engine = self::newDifferentialMarkupEngine();
$engine->setConfig('viewer', $viewer);
foreach ($content_blocks as $content_block) {
$engine->markupText($content_block);
$phids = $engine->getTextMetadata(
PhabricatorEmbedFileRemarkupRule::KEY_EMBED_FILE_PHIDS,
array());
foreach ($phids as $phid) {
$files[$phid] = $phid;
}
}
return array_values($files);
}
public static function summarizeSentence($corpus) {
$corpus = trim($corpus);
$blocks = preg_split('/\n+/', $corpus, 2);
$block = head($blocks);
$sentences = preg_split(
'/\b([.?!]+)\B/u',
$block,
2,
PREG_SPLIT_DELIM_CAPTURE);
if (count($sentences) > 1) {
$result = $sentences[0].$sentences[1];
} else {
$result = head($sentences);
}
return id(new PhutilUTF8StringTruncator())
->setMaximumGlyphs(128)
->truncateString($result);
}
/**
* Produce a corpus summary, in a way that shortens the underlying text
* without truncating it somewhere awkward.
*
* TODO: We could do a better job of this.
*
* @param string Remarkup corpus to summarize.
* @return string Summarized corpus.
*/
public static function summarize($corpus) {
// Major goals here are:
// - Don't split in the middle of a character (utf-8).
// - Don't split in the middle of, e.g., **bold** text, since
// we end up with hanging '**' in the summary.
// - Try not to pick an image macro, header, embedded file, etc.
// - Hopefully don't return too much text. We don't explicitly limit
// this right now.
$blocks = preg_split("/\n *\n\s*/", $corpus);
$best = null;
foreach ($blocks as $block) {
// This is a test for normal spaces in the block, i.e. a heuristic to
// distinguish standard paragraphs from things like image macros. It may
// not work well for non-latin text. We prefer to summarize with a
// paragraph of normal words over an image macro, if possible.
$has_space = preg_match('/\w\s\w/', $block);
// This is a test to find embedded images and headers. We prefer to
// summarize with a normal paragraph over a header or an embedded object,
// if possible.
$has_embed = preg_match('/^[{=]/', $block);
if ($has_space && !$has_embed) {
// This seems like a good summary, so return it.
return $block;
}
if (!$best) {
// This is the first block we found; if everything is garbage just
// use the first block.
$best = $block;
}
}
return $best;
}
private static function loadCustomInlineRules() {
return id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorRemarkupCustomInlineRule')
->execute();
}
private static function loadCustomBlockRules() {
return id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorRemarkupCustomBlockRule')
->execute();
}
public static function digestRemarkupContent($object, $content) {
$parts = array();
$parts[] = get_class($object);
if ($object instanceof PhabricatorLiskDAO) {
$parts[] = $object->getID();
}
$parts[] = $content;
$message = implode("\n", $parts);
return PhabricatorHash::digestWithNamedKey($message, 'remarkup');
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php b/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php
index cbf322b2d..9d79d223e 100644
--- a/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php
+++ b/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php
@@ -1,60 +1,70 @@
<?php
final class PhabricatorYoutubeRemarkupRule extends PhutilRemarkupRule {
public function getPriority() {
return 350.0;
}
public function apply($text) {
try {
$uri = new PhutilURI($text);
} catch (Exception $ex) {
return $text;
}
$domain = $uri->getDomain();
if (!preg_match('/(^|\.)youtube\.com\z/', $domain)) {
return $text;
}
- $params = $uri->getQueryParams();
- $v_param = idx($params, 'v');
- if (!strlen($v_param)) {
+ $v_params = array();
+
+ $params = $uri->getQueryParamsAsPairList();
+ foreach ($params as $pair) {
+ list($k, $v) = $pair;
+ if ($k === 'v') {
+ $v_params[] = $v;
+ }
+ }
+
+ if (count($v_params) !== 1) {
return $text;
}
+ $v_param = head($v_params);
+
$text_mode = $this->getEngine()->isTextMode();
$mail_mode = $this->getEngine()->isHTMLMailMode();
if ($text_mode || $mail_mode) {
return $text;
}
$youtube_src = 'https://www.youtube.com/embed/'.$v_param;
$iframe = $this->newTag(
'div',
array(
'class' => 'embedded-youtube-video',
),
$this->newTag(
'iframe',
array(
'width' => '650',
'height' => '400',
'style' => 'margin: 1em auto; border: 0px;',
'src' => $youtube_src,
'frameborder' => 0,
),
''));
return $this->getEngine()->storeText($iframe);
}
public function didMarkupText() {
CelerityAPI::getStaticResourceResponse()
->addContentSecurityPolicyURI('frame-src', 'https://www.youtube.com/');
}
}
diff --git a/src/infrastructure/query/PhabricatorEmptyQueryException.php b/src/infrastructure/query/exception/PhabricatorEmptyQueryException.php
similarity index 100%
rename from src/infrastructure/query/PhabricatorEmptyQueryException.php
rename to src/infrastructure/query/exception/PhabricatorEmptyQueryException.php
diff --git a/src/infrastructure/query/exception/PhabricatorInvalidQueryCursorException.php b/src/infrastructure/query/exception/PhabricatorInvalidQueryCursorException.php
new file mode 100644
index 000000000..8a87745f9
--- /dev/null
+++ b/src/infrastructure/query/exception/PhabricatorInvalidQueryCursorException.php
@@ -0,0 +1,4 @@
+<?php
+
+final class PhabricatorInvalidQueryCursorException
+ extends Exception {}
diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
index 9f7a69909..f5586fd90 100644
--- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
+++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
@@ -1,2962 +1,3174 @@
<?php
/**
* A query class which uses cursor-based paging. This paging is much more
* performant than offset-based paging in the presence of policy filtering.
*
+ * @task cursors Query Cursors
* @task clauses Building Query Clauses
* @task appsearch Integration with ApplicationSearch
* @task customfield Integration with CustomField
* @task paging Paging
* @task order Result Ordering
* @task edgelogic Working with Edge Logic
* @task spaces Working with Spaces
*/
abstract class PhabricatorCursorPagedPolicyAwareQuery
extends PhabricatorPolicyAwareQuery {
- private $afterID;
- private $beforeID;
+ private $externalCursorString;
+ private $internalCursorObject;
+ private $isQueryOrderReversed = false;
+ private $rawCursorRow;
+
private $applicationSearchConstraints = array();
private $internalPaging;
private $orderVector;
private $groupVector;
private $builtinOrder;
private $edgeLogicConstraints = array();
private $edgeLogicConstraintsAreValid = false;
private $spacePHIDs;
private $spaceIsArchived;
private $ngrams = array();
private $ferretEngine;
private $ferretTokens = array();
private $ferretTables = array();
private $ferretQuery;
private $ferretMetadata = array();
- protected function getPageCursors(array $page) {
+ const FULLTEXT_RANK = '_ft_rank';
+ const FULLTEXT_MODIFIED = '_ft_epochModified';
+ const FULLTEXT_CREATED = '_ft_epochCreated';
+
+/* -( Cursors )------------------------------------------------------------ */
+
+ protected function newExternalCursorStringForResult($object) {
+ if (!($object instanceof LiskDAO)) {
+ throw new Exception(
+ pht(
+ 'Expected to be passed a result object of class "LiskDAO" in '.
+ '"newExternalCursorStringForResult()", actually passed "%s". '.
+ 'Return storage objects from "loadPage()" or override '.
+ '"newExternalCursorStringForResult()".',
+ phutil_describe_type($object)));
+ }
+
+ return (string)$object->getID();
+ }
+
+ protected function newInternalCursorFromExternalCursor($cursor) {
+ $viewer = $this->getViewer();
+
+ $query = newv(get_class($this), array());
+
+ $query
+ ->setParentQuery($this)
+ ->setViewer($viewer);
+
+ // We're copying our order vector to the subquery so that the subquery
+ // knows it should generate any supplemental information required by the
+ // ordering.
+
+ // For example, Phriction documents may be ordered by title, but the title
+ // isn't a column in the "document" table: the query must JOIN the
+ // "content" table to perform the ordering. Passing the ordering to the
+ // subquery tells it that we need it to do that JOIN and attach relevant
+ // paging information to the internal cursor object.
+
+ // We only expect to load a single result, so the actual result order does
+ // not matter. We only want the internal cursor for that result to look
+ // like a cursor this parent query would generate.
+ $query->setOrderVector($this->getOrderVector());
+
+ $this->applyExternalCursorConstraintsToQuery($query, $cursor);
+
+ // If we have a Ferret fulltext query, copy it to the subquery so that we
+ // generate ranking columns appropriately, and compute the correct object
+ // ranking score for the current query.
+ if ($this->ferretEngine) {
+ $query->withFerretConstraint($this->ferretEngine, $this->ferretTokens);
+ }
+
+ // We're executing the subquery normally to make sure the viewer can
+ // actually see the object, and that it's a completely valid object which
+ // passes all filtering and policy checks. You aren't allowed to use an
+ // object you can't see as a cursor, since this can leak information.
+ $result = $query->executeOne();
+ if (!$result) {
+ $this->throwCursorException(
+ pht(
+ 'Cursor "%s" does not identify a valid object in query "%s".',
+ $cursor,
+ get_class($this)));
+ }
+
+ // Now that we made sure the viewer can actually see the object the
+ // external cursor identifies, return the internal cursor the query
+ // generated as a side effect while loading the object.
+ return $query->getInternalCursorObject();
+ }
+
+ final protected function throwCursorException($message) {
+ throw new PhabricatorInvalidQueryCursorException($message);
+ }
+
+ protected function applyExternalCursorConstraintsToQuery(
+ PhabricatorCursorPagedPolicyAwareQuery $subquery,
+ $cursor) {
+ $subquery->withIDs(array($cursor));
+ }
+
+ protected function newPagingMapFromCursorObject(
+ PhabricatorQueryCursor $cursor,
+ array $keys) {
+
+ $object = $cursor->getObject();
+
+ return $this->newPagingMapFromPartialObject($object);
+ }
+
+ protected function newPagingMapFromPartialObject($object) {
return array(
- $this->getResultCursor(head($page)),
- $this->getResultCursor(last($page)),
+ 'id' => (int)$object->getID(),
);
}
- protected function getResultCursor($object) {
- if (!is_object($object)) {
+
+ final private function getExternalCursorStringForResult($object) {
+ $cursor = $this->newExternalCursorStringForResult($object);
+
+ if (!is_string($cursor)) {
throw new Exception(
pht(
- 'Expected object, got "%s".',
- gettype($object)));
+ 'Expected "newExternalCursorStringForResult()" in class "%s" to '.
+ 'return a string, but got "%s".',
+ get_class($this),
+ phutil_describe_type($cursor)));
}
- return $object->getID();
+ return $cursor;
}
- protected function nextPage(array $page) {
- // See getPagingViewer() for a description of this flag.
- $this->internalPaging = true;
+ final private function getExternalCursorString() {
+ return $this->externalCursorString;
+ }
- if ($this->beforeID !== null) {
- $page = array_reverse($page, $preserve_keys = true);
- list($before, $after) = $this->getPageCursors($page);
- $this->beforeID = $before;
- } else {
- list($before, $after) = $this->getPageCursors($page);
- $this->afterID = $after;
- }
+ final private function setExternalCursorString($external_cursor) {
+ $this->externalCursorString = $external_cursor;
+ return $this;
+ }
+
+ final private function getIsQueryOrderReversed() {
+ return $this->isQueryOrderReversed;
}
- final public function setAfterID($object_id) {
- $this->afterID = $object_id;
+ final private function setIsQueryOrderReversed($is_reversed) {
+ $this->isQueryOrderReversed = $is_reversed;
return $this;
}
- final protected function getAfterID() {
- return $this->afterID;
+ final private function getInternalCursorObject() {
+ return $this->internalCursorObject;
}
- final public function setBeforeID($object_id) {
- $this->beforeID = $object_id;
+ final private function setInternalCursorObject(
+ PhabricatorQueryCursor $cursor) {
+ $this->internalCursorObject = $cursor;
return $this;
}
- final protected function getBeforeID() {
- return $this->beforeID;
+ final private function getInternalCursorFromExternalCursor(
+ $cursor_string) {
+
+ $cursor_object = $this->newInternalCursorFromExternalCursor($cursor_string);
+
+ if (!($cursor_object instanceof PhabricatorQueryCursor)) {
+ throw new Exception(
+ pht(
+ 'Expected "newInternalCursorFromExternalCursor()" to return an '.
+ 'object of class "PhabricatorQueryCursor", but got "%s" (in '.
+ 'class "%s").',
+ phutil_describe_type($cursor_object),
+ get_class($this)));
+ }
+
+ return $cursor_object;
+ }
+
+ final private function getPagingMapFromCursorObject(
+ PhabricatorQueryCursor $cursor,
+ array $keys) {
+
+ $map = $this->newPagingMapFromCursorObject($cursor, $keys);
+
+ if (!is_array($map)) {
+ throw new Exception(
+ pht(
+ 'Expected "newPagingMapFromCursorObject()" to return a map of '.
+ 'paging values, but got "%s" (in class "%s").',
+ phutil_describe_type($map),
+ get_class($this)));
+ }
+
+ if ($this->supportsFerretEngine()) {
+ if ($this->hasFerretOrder()) {
+ $map += array(
+ 'rank' =>
+ $cursor->getRawRowProperty(self::FULLTEXT_RANK),
+ 'fulltext-modified' =>
+ $cursor->getRawRowProperty(self::FULLTEXT_MODIFIED),
+ 'fulltext-created' =>
+ $cursor->getRawRowProperty(self::FULLTEXT_CREATED),
+ );
+ }
+ }
+
+ foreach ($keys as $key) {
+ if (!array_key_exists($key, $map)) {
+ throw new Exception(
+ pht(
+ 'Map returned by "newPagingMapFromCursorObject()" in class "%s" '.
+ 'omits required key "%s".',
+ get_class($this),
+ $key));
+ }
+ }
+
+ return $map;
+ }
+
+ final protected function nextPage(array $page) {
+ if (!$page) {
+ return;
+ }
+
+ $cursor = id(new PhabricatorQueryCursor())
+ ->setObject(last($page));
+
+ if ($this->rawCursorRow) {
+ $cursor->setRawRow($this->rawCursorRow);
+ }
+
+ $this->setInternalCursorObject($cursor);
}
final public function getFerretMetadata() {
if (!$this->supportsFerretEngine()) {
throw new Exception(
pht(
'Unable to retrieve Ferret engine metadata, this class ("%s") does '.
'not support the Ferret engine.',
get_class($this)));
}
return $this->ferretMetadata;
}
protected function loadStandardPage(PhabricatorLiskDAO $table) {
$rows = $this->loadStandardPageRows($table);
return $table->loadAllFromArray($rows);
}
protected function loadStandardPageRows(PhabricatorLiskDAO $table) {
$conn = $table->establishConnection('r');
return $this->loadStandardPageRowsWithConnection(
$conn,
$table->getTableName());
}
protected function loadStandardPageRowsWithConnection(
AphrontDatabaseConnection $conn,
$table_name) {
$query = $this->buildStandardPageQuery($conn, $table_name);
$rows = queryfx_all($conn, '%Q', $query);
$rows = $this->didLoadRawRows($rows);
return $rows;
}
protected function buildStandardPageQuery(
AphrontDatabaseConnection $conn,
$table_name) {
$table_alias = $this->getPrimaryTableAlias();
if ($table_alias === null) {
$table_alias = qsprintf($conn, '');
} else {
$table_alias = qsprintf($conn, '%T', $table_alias);
}
return qsprintf(
$conn,
'%Q FROM %T %Q %Q %Q %Q %Q %Q %Q',
$this->buildSelectClause($conn),
$table_name,
$table_alias,
$this->buildJoinClause($conn),
$this->buildWhereClause($conn),
$this->buildGroupClause($conn),
$this->buildHavingClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
}
protected function didLoadRawRows(array $rows) {
+ $this->rawCursorRow = last($rows);
+
if ($this->ferretEngine) {
foreach ($rows as $row) {
$phid = $row['phid'];
$metadata = id(new PhabricatorFerretMetadata())
->setPHID($phid)
->setEngine($this->ferretEngine)
- ->setRelevance(idx($row, '_ft_rank'));
+ ->setRelevance(idx($row, self::FULLTEXT_RANK));
$this->ferretMetadata[$phid] = $metadata;
- unset($row['_ft_rank']);
+ unset($row[self::FULLTEXT_RANK]);
+ unset($row[self::FULLTEXT_MODIFIED]);
+ unset($row[self::FULLTEXT_CREATED]);
}
}
return $rows;
}
- /**
- * Get the viewer for making cursor paging queries.
- *
- * NOTE: You should ONLY use this viewer to load cursor objects while
- * building paging queries.
- *
- * Cursor paging can happen in two ways. First, the user can request a page
- * like `/stuff/?after=33`, which explicitly causes paging. Otherwise, we
- * can fall back to implicit paging if we filter some results out of a
- * result list because the user can't see them and need to go fetch some more
- * results to generate a large enough result list.
- *
- * In the first case, want to use the viewer's policies to load the object.
- * This prevents an attacker from figuring out information about an object
- * they can't see by executing queries like `/stuff/?after=33&order=name`,
- * which would otherwise give them a hint about the name of the object.
- * Generally, if a user can't see an object, they can't use it to page.
- *
- * In the second case, we need to load the object whether the user can see
- * it or not, because we need to examine new results. For example, if a user
- * loads `/stuff/` and we run a query for the first 100 items that they can
- * see, but the first 100 rows in the database aren't visible, we need to
- * be able to issue a query for the next 100 results. If we can't load the
- * cursor object, we'll fail or issue the same query over and over again.
- * So, generally, internal paging must bypass policy controls.
- *
- * This method returns the appropriate viewer, based on the context in which
- * the paging is occurring.
- *
- * @return PhabricatorUser Viewer for executing paging queries.
- */
- final protected function getPagingViewer() {
- if ($this->internalPaging) {
- return PhabricatorUser::getOmnipotentUser();
- } else {
- return $this->getViewer();
- }
- }
-
final protected function buildLimitClause(AphrontDatabaseConnection $conn) {
if ($this->shouldLimitResults()) {
$limit = $this->getRawResultLimit();
if ($limit) {
return qsprintf($conn, 'LIMIT %d', $limit);
}
}
return qsprintf($conn, '');
}
protected function shouldLimitResults() {
return true;
}
final protected function didLoadResults(array $results) {
- if ($this->beforeID) {
+ if ($this->getIsQueryOrderReversed()) {
$results = array_reverse($results, $preserve_keys = true);
}
return $results;
}
final public function executeWithCursorPager(AphrontCursorPagerView $pager) {
$limit = $pager->getPageSize();
$this->setLimit($limit + 1);
- if ($pager->getAfterID()) {
- $this->setAfterID($pager->getAfterID());
+ if (strlen($pager->getAfterID())) {
+ $this->setExternalCursorString($pager->getAfterID());
} else if ($pager->getBeforeID()) {
- $this->setBeforeID($pager->getBeforeID());
+ $this->setExternalCursorString($pager->getBeforeID());
+ $this->setIsQueryOrderReversed(true);
}
$results = $this->execute();
$count = count($results);
$sliced_results = $pager->sliceResults($results);
if ($sliced_results) {
- list($before, $after) = $this->getPageCursors($sliced_results);
+
+ // If we have results, generate external-facing cursors from the visible
+ // results. This stops us from leaking any internal details about objects
+ // which we loaded but which were not visible to the viewer.
if ($pager->getBeforeID() || ($count > $limit)) {
- $pager->setNextPageID($after);
+ $last_object = last($sliced_results);
+ $cursor = $this->getExternalCursorStringForResult($last_object);
+ $pager->setNextPageID($cursor);
}
if ($pager->getAfterID() ||
($pager->getBeforeID() && ($count > $limit))) {
- $pager->setPrevPageID($before);
+ $head_object = head($sliced_results);
+ $cursor = $this->getExternalCursorStringForResult($head_object);
+ $pager->setPrevPageID($cursor);
}
}
return $sliced_results;
}
/**
* Return the alias this query uses to identify the primary table.
*
* Some automatic query constructions may need to be qualified with a table
* alias if the query performs joins which make column names ambiguous. If
* this is the case, return the alias for the primary table the query
* uses; generally the object table which has `id` and `phid` columns.
*
* @return string Alias for the primary table.
*/
protected function getPrimaryTableAlias() {
return null;
}
public function newResultObject() {
return null;
}
/* -( Building Query Clauses )--------------------------------------------- */
/**
* @task clauses
*/
protected function buildSelectClause(AphrontDatabaseConnection $conn) {
$parts = $this->buildSelectClauseParts($conn);
return $this->formatSelectClause($conn, $parts);
}
/**
* @task clauses
*/
protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
$select = array();
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$select[] = qsprintf($conn, '%T.*', $alias);
} else {
$select[] = qsprintf($conn, '*');
}
$select[] = $this->buildEdgeLogicSelectClause($conn);
$select[] = $this->buildFerretSelectClause($conn);
return $select;
}
/**
* @task clauses
*/
protected function buildJoinClause(AphrontDatabaseConnection $conn) {
$joins = $this->buildJoinClauseParts($conn);
return $this->formatJoinClause($conn, $joins);
}
/**
* @task clauses
*/
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = array();
$joins[] = $this->buildEdgeLogicJoinClause($conn);
$joins[] = $this->buildApplicationSearchJoinClause($conn);
$joins[] = $this->buildNgramsJoinClause($conn);
$joins[] = $this->buildFerretJoinClause($conn);
return $joins;
}
/**
* @task clauses
*/
protected function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = $this->buildWhereClauseParts($conn);
return $this->formatWhereClause($conn, $where);
}
/**
* @task clauses
*/
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = array();
- $where[] = $this->buildPagingClause($conn);
+ $where[] = $this->buildPagingWhereClause($conn);
$where[] = $this->buildEdgeLogicWhereClause($conn);
$where[] = $this->buildSpacesWhereClause($conn);
$where[] = $this->buildNgramsWhereClause($conn);
$where[] = $this->buildFerretWhereClause($conn);
$where[] = $this->buildApplicationSearchWhereClause($conn);
return $where;
}
/**
* @task clauses
*/
protected function buildHavingClause(AphrontDatabaseConnection $conn) {
$having = $this->buildHavingClauseParts($conn);
+ $having[] = $this->buildPagingHavingClause($conn);
return $this->formatHavingClause($conn, $having);
}
/**
* @task clauses
*/
protected function buildHavingClauseParts(AphrontDatabaseConnection $conn) {
$having = array();
$having[] = $this->buildEdgeLogicHavingClause($conn);
return $having;
}
/**
* @task clauses
*/
protected function buildGroupClause(AphrontDatabaseConnection $conn) {
if (!$this->shouldGroupQueryResultRows()) {
return qsprintf($conn, '');
}
return qsprintf(
$conn,
'GROUP BY %Q',
$this->getApplicationSearchObjectPHIDColumn($conn));
}
/**
* @task clauses
*/
protected function shouldGroupQueryResultRows() {
if ($this->shouldGroupEdgeLogicResultRows()) {
return true;
}
if ($this->getApplicationSearchMayJoinMultipleRows()) {
return true;
}
if ($this->shouldGroupNgramResultRows()) {
return true;
}
if ($this->shouldGroupFerretResultRows()) {
return true;
}
return false;
}
/* -( Paging )------------------------------------------------------------- */
+ private function buildPagingWhereClause(AphrontDatabaseConnection $conn) {
+ if ($this->shouldPageWithHavingClause()) {
+ return null;
+ }
+
+ return $this->buildPagingClause($conn);
+ }
+
+ private function buildPagingHavingClause(AphrontDatabaseConnection $conn) {
+ if (!$this->shouldPageWithHavingClause()) {
+ return null;
+ }
+
+ return $this->buildPagingClause($conn);
+ }
+
+ private function shouldPageWithHavingClause() {
+ // If any of the paging conditions reference dynamic columns, we need to
+ // put the paging conditions in a "HAVING" clause instead of a "WHERE"
+ // clause.
+
+ // For example, this happens when paging on the Ferret "rank" column,
+ // since the "rank" value is computed dynamically in the SELECT statement.
+
+ $orderable = $this->getOrderableColumns();
+ $vector = $this->getOrderVector();
+
+ foreach ($vector as $order) {
+ $key = $order->getOrderKey();
+ $column = $orderable[$key];
+
+ if (!empty($column['having'])) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
/**
* @task paging
*/
protected function buildPagingClause(AphrontDatabaseConnection $conn) {
$orderable = $this->getOrderableColumns();
- $vector = $this->getOrderVector();
+ $vector = $this->getQueryableOrderVector();
- if ($this->beforeID !== null) {
- $cursor = $this->beforeID;
- $reversed = true;
- } else if ($this->afterID !== null) {
- $cursor = $this->afterID;
- $reversed = false;
- } else {
- // No paging is being applied to this query so we do not need to
- // construct a paging clause.
+ // If we don't have a cursor object yet, it means we're trying to load
+ // the first result page. We may need to build a cursor object from the
+ // external string, or we may not need a paging clause yet.
+ $cursor_object = $this->getInternalCursorObject();
+ if (!$cursor_object) {
+ $external_cursor = $this->getExternalCursorString();
+ if ($external_cursor !== null) {
+ $cursor_object = $this->getInternalCursorFromExternalCursor(
+ $external_cursor);
+ }
+ }
+
+ // If we still don't have a cursor object, this is the first result page
+ // and we aren't paging it. We don't need to build a paging clause.
+ if (!$cursor_object) {
return qsprintf($conn, '');
}
+ $reversed = $this->getIsQueryOrderReversed();
+
$keys = array();
foreach ($vector as $order) {
$keys[] = $order->getOrderKey();
}
+ $keys = array_fuse($keys);
- $value_map = $this->getPagingValueMap($cursor, $keys);
+ $value_map = $this->getPagingMapFromCursorObject(
+ $cursor_object,
+ $keys);
$columns = array();
foreach ($vector as $order) {
$key = $order->getOrderKey();
- if (!array_key_exists($key, $value_map)) {
- throw new Exception(
- pht(
- 'Query "%s" failed to return a value from getPagingValueMap() '.
- 'for column "%s".',
- get_class($this),
- $key));
- }
-
$column = $orderable[$key];
$column['value'] = $value_map[$key];
// If the vector component is reversed, we need to reverse whatever the
// order of the column is.
if ($order->getIsReversed()) {
$column['reverse'] = !idx($column, 'reverse', false);
}
$columns[] = $column;
}
return $this->buildPagingClauseFromMultipleColumns(
$conn,
$columns,
array(
'reversed' => $reversed,
));
}
- /**
- * @task paging
- */
- protected function getPagingValueMap($cursor, array $keys) {
- return array(
- 'id' => $cursor,
- );
- }
-
-
- /**
- * @task paging
- */
- protected function loadCursorObject($cursor) {
- $query = newv(get_class($this), array())
- ->setViewer($this->getPagingViewer())
- ->withIDs(array((int)$cursor));
-
- $this->willExecuteCursorQuery($query);
-
- $object = $query->executeOne();
- if (!$object) {
- throw new Exception(
- pht(
- 'Cursor "%s" does not identify a valid object in query "%s".',
- $cursor,
- get_class($this)));
- }
-
- return $object;
- }
-
-
- /**
- * @task paging
- */
- protected function willExecuteCursorQuery(
- PhabricatorCursorPagedPolicyAwareQuery $query) {
- return;
- }
-
-
/**
* Simplifies the task of constructing a paging clause across multiple
* columns. In the general case, this looks like:
*
* A > a OR (A = a AND B > b) OR (A = a AND B = b AND C > c)
*
* To build a clause, specify the name, type, and value of each column
* to include:
*
* $this->buildPagingClauseFromMultipleColumns(
* $conn_r,
* array(
* array(
* 'table' => 't',
* 'column' => 'title',
* 'type' => 'string',
* 'value' => $cursor->getTitle(),
* 'reverse' => true,
* ),
* array(
* 'table' => 't',
* 'column' => 'id',
* 'type' => 'int',
* 'value' => $cursor->getID(),
* ),
* ),
* array(
* 'reversed' => $is_reversed,
* ));
*
* This method will then return a composable clause for inclusion in WHERE.
*
* @param AphrontDatabaseConnection Connection query will execute on.
* @param list<map> Column description dictionaries.
* @param map Additional construction options.
* @return string Query clause.
* @task paging
*/
final protected function buildPagingClauseFromMultipleColumns(
AphrontDatabaseConnection $conn,
array $columns,
array $options) {
foreach ($columns as $column) {
PhutilTypeSpec::checkMap(
$column,
array(
'table' => 'optional string|null',
'column' => 'string',
'value' => 'wild',
'type' => 'string',
'reverse' => 'optional bool',
'unique' => 'optional bool',
'null' => 'optional string|null',
+ 'requires-ferret' => 'optional bool',
+ 'having' => 'optional bool',
));
}
PhutilTypeSpec::checkMap(
$options,
array(
'reversed' => 'optional bool',
));
$is_query_reversed = idx($options, 'reversed', false);
$clauses = array();
$accumulated = array();
$last_key = last_key($columns);
foreach ($columns as $key => $column) {
$type = $column['type'];
$null = idx($column, 'null');
if ($column['value'] === null) {
if ($null) {
$value = null;
} else {
throw new Exception(
pht(
'Column "%s" has null value, but does not specify a null '.
'behavior.',
$key));
}
} else {
switch ($type) {
case 'int':
$value = qsprintf($conn, '%d', $column['value']);
break;
case 'float':
$value = qsprintf($conn, '%f', $column['value']);
break;
case 'string':
$value = qsprintf($conn, '%s', $column['value']);
break;
default:
throw new Exception(
pht(
'Column "%s" has unknown column type "%s".',
$column['column'],
$type));
}
}
$is_column_reversed = idx($column, 'reverse', false);
$reverse = ($is_query_reversed xor $is_column_reversed);
$clause = $accumulated;
$table_name = idx($column, 'table');
$column_name = $column['column'];
if ($table_name !== null) {
$field = qsprintf($conn, '%T.%T', $table_name, $column_name);
} else {
$field = qsprintf($conn, '%T', $column_name);
}
$parts = array();
if ($null) {
$can_page_if_null = ($null === 'head');
$can_page_if_nonnull = ($null === 'tail');
if ($reverse) {
$can_page_if_null = !$can_page_if_null;
$can_page_if_nonnull = !$can_page_if_nonnull;
}
$subclause = null;
if ($can_page_if_null && $value === null) {
$parts[] = qsprintf(
$conn,
'(%Q IS NOT NULL)',
$field);
} else if ($can_page_if_nonnull && $value !== null) {
$parts[] = qsprintf(
$conn,
'(%Q IS NULL)',
$field);
}
}
if ($value !== null) {
$parts[] = qsprintf(
$conn,
'%Q %Q %Q',
$field,
$reverse ? qsprintf($conn, '>') : qsprintf($conn, '<'),
$value);
}
if ($parts) {
$clause[] = qsprintf($conn, '%LO', $parts);
}
if ($clause) {
$clauses[] = qsprintf($conn, '%LA', $clause);
}
if ($value === null) {
$accumulated[] = qsprintf(
$conn,
'%Q IS NULL',
$field);
} else {
$accumulated[] = qsprintf(
$conn,
'%Q = %Q',
$field,
$value);
}
}
if ($clauses) {
return qsprintf($conn, '%LO', $clauses);
}
return qsprintf($conn, '');
}
/* -( Result Ordering )---------------------------------------------------- */
/**
* Select a result ordering.
*
* This is a high-level method which selects an ordering from a predefined
* list of builtin orders, as provided by @{method:getBuiltinOrders}. These
* options are user-facing and not exhaustive, but are generally convenient
* and meaningful.
*
* You can also use @{method:setOrderVector} to specify a low-level ordering
* across individual orderable columns. This offers greater control but is
* also more involved.
*
* @param string Key of a builtin order supported by this query.
* @return this
* @task order
*/
public function setOrder($order) {
$aliases = $this->getBuiltinOrderAliasMap();
if (empty($aliases[$order])) {
throw new Exception(
pht(
'Query "%s" does not support a builtin order "%s". Supported orders '.
'are: %s.',
get_class($this),
$order,
implode(', ', array_keys($aliases))));
}
$this->builtinOrder = $aliases[$order];
$this->orderVector = null;
return $this;
}
/**
* Set a grouping order to apply before primary result ordering.
*
* This allows you to preface the query order vector with additional orders,
* so you can effect "group by" queries while still respecting "order by".
*
* This is a high-level method which works alongside @{method:setOrder}. For
* lower-level control over order vectors, use @{method:setOrderVector}.
*
* @param PhabricatorQueryOrderVector|list<string> List of order keys.
* @return this
* @task order
*/
public function setGroupVector($vector) {
$this->groupVector = $vector;
$this->orderVector = null;
return $this;
}
/**
* Get builtin orders for this class.
*
* In application UIs, we want to be able to present users with a small
* selection of meaningful order options (like "Order by Title") rather than
* an exhaustive set of column ordering options.
*
* Meaningful user-facing orders are often really orders across multiple
* columns: for example, a "title" ordering is usually implemented as a
* "title, id" ordering under the hood.
*
* Builtin orders provide a mapping from convenient, understandable
* user-facing orders to implementations.
*
* A builtin order should provide these keys:
*
* - `vector` (`list<string>`): The actual order vector to use.
* - `name` (`string`): Human-readable order name.
*
* @return map<string, wild> Map from builtin order keys to specification.
* @task order
*/
public function getBuiltinOrders() {
$orders = array(
'newest' => array(
'vector' => array('id'),
'name' => pht('Creation (Newest First)'),
'aliases' => array('created'),
),
'oldest' => array(
'vector' => array('-id'),
'name' => pht('Creation (Oldest First)'),
),
);
$object = $this->newResultObject();
if ($object instanceof PhabricatorCustomFieldInterface) {
$list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
foreach ($list->getFields() as $field) {
$index = $field->buildOrderIndex();
if (!$index) {
continue;
}
$legacy_key = 'custom:'.$field->getFieldKey();
$modern_key = $field->getModernFieldKey();
$orders[$modern_key] = array(
'vector' => array($modern_key, 'id'),
'name' => $field->getFieldName(),
'aliases' => array($legacy_key),
);
$orders['-'.$modern_key] = array(
'vector' => array('-'.$modern_key, '-id'),
'name' => pht('%s (Reversed)', $field->getFieldName()),
);
}
}
if ($this->supportsFerretEngine()) {
$orders['relevance'] = array(
'vector' => array('rank', 'fulltext-modified', 'id'),
'name' => pht('Relevance'),
);
}
return $orders;
}
public function getBuiltinOrderAliasMap() {
$orders = $this->getBuiltinOrders();
$map = array();
foreach ($orders as $key => $order) {
$keys = array();
$keys[] = $key;
foreach (idx($order, 'aliases', array()) as $alias) {
$keys[] = $alias;
}
foreach ($keys as $alias) {
if (isset($map[$alias])) {
throw new Exception(
pht(
'Two builtin orders ("%s" and "%s") define the same key or '.
'alias ("%s"). Each order alias and key must be unique and '.
'identify a single order.',
$key,
$map[$alias],
$alias));
}
$map[$alias] = $key;
}
}
return $map;
}
/**
* Set a low-level column ordering.
*
* This is a low-level method which offers granular control over column
* ordering. In most cases, applications can more easily use
* @{method:setOrder} to choose a high-level builtin order.
*
* To set an order vector, specify a list of order keys as provided by
* @{method:getOrderableColumns}.
*
* @param PhabricatorQueryOrderVector|list<string> List of order keys.
* @return this
* @task order
*/
public function setOrderVector($vector) {
$vector = PhabricatorQueryOrderVector::newFromVector($vector);
$orderable = $this->getOrderableColumns();
// Make sure that all the components identify valid columns.
$unique = array();
foreach ($vector as $order) {
$key = $order->getOrderKey();
if (empty($orderable[$key])) {
$valid = implode(', ', array_keys($orderable));
throw new Exception(
pht(
'This query ("%s") does not support sorting by order key "%s". '.
'Supported orders are: %s.',
get_class($this),
$key,
$valid));
}
$unique[$key] = idx($orderable[$key], 'unique', false);
}
// Make sure that the last column is unique so that this is a strong
// ordering which can be used for paging.
$last = last($unique);
if ($last !== true) {
throw new Exception(
pht(
'Order vector "%s" is invalid: the last column in an order must '.
'be a column with unique values, but "%s" is not unique.',
$vector->getAsString(),
last_key($unique)));
}
// Make sure that other columns are not unique; an ordering like "id, name"
// does not make sense because only "id" can ever have an effect.
array_pop($unique);
foreach ($unique as $key => $is_unique) {
if ($is_unique) {
throw new Exception(
pht(
'Order vector "%s" is invalid: only the last column in an order '.
'may be unique, but "%s" is a unique column and not the last '.
'column in the order.',
$vector->getAsString(),
$key));
}
}
$this->orderVector = $vector;
return $this;
}
/**
* Get the effective order vector.
*
* @return PhabricatorQueryOrderVector Effective vector.
* @task order
*/
protected function getOrderVector() {
if (!$this->orderVector) {
if ($this->builtinOrder !== null) {
$builtin_order = idx($this->getBuiltinOrders(), $this->builtinOrder);
$vector = $builtin_order['vector'];
} else {
$vector = $this->getDefaultOrderVector();
}
if ($this->groupVector) {
$group = PhabricatorQueryOrderVector::newFromVector($this->groupVector);
$group->appendVector($vector);
$vector = $group;
}
$vector = PhabricatorQueryOrderVector::newFromVector($vector);
// We call setOrderVector() here to apply checks to the default vector.
// This catches any errors in the implementation.
$this->setOrderVector($vector);
}
return $this->orderVector;
}
/**
* @task order
*/
protected function getDefaultOrderVector() {
return array('id');
}
/**
* @task order
*/
public function getOrderableColumns() {
$cache = PhabricatorCaches::getRequestCache();
$class = get_class($this);
$cache_key = 'query.orderablecolumns.'.$class;
$columns = $cache->getKey($cache_key);
if ($columns !== null) {
return $columns;
}
$columns = array(
'id' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'id',
'reverse' => false,
'type' => 'int',
'unique' => true,
),
);
$object = $this->newResultObject();
if ($object instanceof PhabricatorCustomFieldInterface) {
$list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
foreach ($list->getFields() as $field) {
$index = $field->buildOrderIndex();
if (!$index) {
continue;
}
$digest = $field->getFieldIndex();
$key = $field->getModernFieldKey();
$columns[$key] = array(
'table' => 'appsearch_order_'.$digest,
'column' => 'indexValue',
'type' => $index->getIndexValueType(),
'null' => 'tail',
'customfield' => true,
'customfield.index.table' => $index->getTableName(),
'customfield.index.key' => $digest,
);
}
}
if ($this->supportsFerretEngine()) {
$columns['rank'] = array(
'table' => null,
- 'column' => '_ft_rank',
+ 'column' => self::FULLTEXT_RANK,
'type' => 'int',
+ 'requires-ferret' => true,
+ 'having' => true,
);
$columns['fulltext-created'] = array(
- 'table' => 'ft_doc',
- 'column' => 'epochCreated',
+ 'table' => null,
+ 'column' => self::FULLTEXT_CREATED,
'type' => 'int',
+ 'requires-ferret' => true,
);
$columns['fulltext-modified'] = array(
- 'table' => 'ft_doc',
- 'column' => 'epochModified',
+ 'table' => null,
+ 'column' => self::FULLTEXT_MODIFIED,
'type' => 'int',
+ 'requires-ferret' => true,
);
}
$cache->setKey($cache_key, $columns);
return $columns;
}
/**
* @task order
*/
final protected function buildOrderClause(
AphrontDatabaseConnection $conn,
$for_union = false) {
$orderable = $this->getOrderableColumns();
- $vector = $this->getOrderVector();
+ $vector = $this->getQueryableOrderVector();
$parts = array();
foreach ($vector as $order) {
$part = $orderable[$order->getOrderKey()];
+
if ($order->getIsReversed()) {
$part['reverse'] = !idx($part, 'reverse', false);
}
$parts[] = $part;
}
return $this->formatOrderClause($conn, $parts, $for_union);
}
+ /**
+ * @task order
+ */
+ private function getQueryableOrderVector() {
+ $vector = $this->getOrderVector();
+ $orderable = $this->getOrderableColumns();
+
+ $keep = array();
+ foreach ($vector as $order) {
+ $column = $orderable[$order->getOrderKey()];
+
+ // If this is a Ferret fulltext column but the query doesn't actually
+ // have a fulltext query, we'll skip most of the Ferret stuff and won't
+ // actually have the columns in the result set. Just skip them.
+ if (!empty($column['requires-ferret'])) {
+ if (!$this->getFerretTokens()) {
+ continue;
+ }
+ }
+
+ $keep[] = $order->getAsScalar();
+ }
+
+ return PhabricatorQueryOrderVector::newFromVector($keep);
+ }
/**
* @task order
*/
protected function formatOrderClause(
AphrontDatabaseConnection $conn,
array $parts,
$for_union = false) {
- $is_query_reversed = false;
- if ($this->getBeforeID()) {
- $is_query_reversed = !$is_query_reversed;
- }
+ $is_query_reversed = $this->getIsQueryOrderReversed();
$sql = array();
foreach ($parts as $key => $part) {
$is_column_reversed = !empty($part['reverse']);
$descending = true;
if ($is_query_reversed) {
$descending = !$descending;
}
if ($is_column_reversed) {
$descending = !$descending;
}
$table = idx($part, 'table');
// When we're building an ORDER BY clause for a sequence of UNION
// statements, we can't refer to tables from the subqueries.
if ($for_union) {
$table = null;
}
$column = $part['column'];
if ($table !== null) {
$field = qsprintf($conn, '%T.%T', $table, $column);
} else {
$field = qsprintf($conn, '%T', $column);
}
$null = idx($part, 'null');
if ($null) {
switch ($null) {
case 'head':
$null_field = qsprintf($conn, '(%Q IS NULL)', $field);
break;
case 'tail':
$null_field = qsprintf($conn, '(%Q IS NOT NULL)', $field);
break;
default:
throw new Exception(
pht(
'NULL value "%s" is invalid. Valid values are "head" and '.
'"tail".',
$null));
}
if ($descending) {
$sql[] = qsprintf($conn, '%Q DESC', $null_field);
} else {
$sql[] = qsprintf($conn, '%Q ASC', $null_field);
}
}
if ($descending) {
$sql[] = qsprintf($conn, '%Q DESC', $field);
} else {
$sql[] = qsprintf($conn, '%Q ASC', $field);
}
}
return qsprintf($conn, 'ORDER BY %LQ', $sql);
}
/* -( Application Search )------------------------------------------------- */
/**
* Constrain the query with an ApplicationSearch index, requiring field values
* contain at least one of the values in a set.
*
* This constraint can build the most common types of queries, like:
*
* - Find users with shirt sizes "X" or "XL".
* - Find shoes with size "13".
*
* @param PhabricatorCustomFieldIndexStorage Table where the index is stored.
* @param string|list<string> One or more values to filter by.
* @return this
* @task appsearch
*/
public function withApplicationSearchContainsConstraint(
PhabricatorCustomFieldIndexStorage $index,
$value) {
$values = (array)$value;
$data_values = array();
$constraint_values = array();
foreach ($values as $value) {
if ($value instanceof PhabricatorQueryConstraint) {
$constraint_values[] = $value;
} else {
$data_values[] = $value;
}
}
$alias = 'appsearch_'.count($this->applicationSearchConstraints);
$this->applicationSearchConstraints[] = array(
'type' => $index->getIndexValueType(),
'cond' => '=',
'table' => $index->getTableName(),
'index' => $index->getIndexKey(),
'alias' => $alias,
'value' => $values,
'data' => $data_values,
'constraints' => $constraint_values,
);
return $this;
}
/**
* Constrain the query with an ApplicationSearch index, requiring values
* exist in a given range.
*
* This constraint is useful for expressing date ranges:
*
* - Find events between July 1st and July 7th.
*
* The ends of the range are inclusive, so a `$min` of `3` and a `$max` of
* `5` will match fields with values `3`, `4`, or `5`. Providing `null` for
* either end of the range will leave that end of the constraint open.
*
* @param PhabricatorCustomFieldIndexStorage Table where the index is stored.
* @param int|null Minimum permissible value, inclusive.
* @param int|null Maximum permissible value, inclusive.
* @return this
* @task appsearch
*/
public function withApplicationSearchRangeConstraint(
PhabricatorCustomFieldIndexStorage $index,
$min,
$max) {
$index_type = $index->getIndexValueType();
if ($index_type != 'int') {
throw new Exception(
pht(
'Attempting to apply a range constraint to a field with index type '.
'"%s", expected type "%s".',
$index_type,
'int'));
}
$alias = 'appsearch_'.count($this->applicationSearchConstraints);
$this->applicationSearchConstraints[] = array(
'type' => $index->getIndexValueType(),
'cond' => 'range',
'table' => $index->getTableName(),
'index' => $index->getIndexKey(),
'alias' => $alias,
'value' => array($min, $max),
+ 'data' => null,
+ 'constraints' => null,
);
return $this;
}
/**
* Get the name of the query's primary object PHID column, for constructing
* JOIN clauses. Normally (and by default) this is just `"phid"`, but it may
* be something more exotic.
*
* See @{method:getPrimaryTableAlias} if the column needs to be qualified with
* a table alias.
*
* @param AphrontDatabaseConnection Connection executing queries.
* @return PhutilQueryString Column name.
* @task appsearch
*/
protected function getApplicationSearchObjectPHIDColumn(
AphrontDatabaseConnection $conn) {
if ($this->getPrimaryTableAlias()) {
return qsprintf($conn, '%T.phid', $this->getPrimaryTableAlias());
} else {
return qsprintf($conn, 'phid');
}
}
/**
* Determine if the JOINs built by ApplicationSearch might cause each primary
* object to return multiple result rows. Generally, this means the query
* needs an extra GROUP BY clause.
*
* @return bool True if the query may return multiple rows for each object.
* @task appsearch
*/
protected function getApplicationSearchMayJoinMultipleRows() {
foreach ($this->applicationSearchConstraints as $constraint) {
$type = $constraint['type'];
$value = $constraint['value'];
$cond = $constraint['cond'];
switch ($cond) {
case '=':
switch ($type) {
case 'string':
case 'int':
if (count($value) > 1) {
return true;
}
break;
default:
throw new Exception(pht('Unknown index type "%s"!', $type));
}
break;
case 'range':
// NOTE: It's possible to write a custom field where multiple rows
// match a range constraint, but we don't currently ship any in the
// upstream and I can't immediately come up with cases where this
// would make sense.
break;
default:
throw new Exception(pht('Unknown constraint condition "%s"!', $cond));
}
}
return false;
}
/**
* Construct a GROUP BY clause appropriate for ApplicationSearch constraints.
*
* @param AphrontDatabaseConnection Connection executing the query.
* @return string Group clause.
* @task appsearch
*/
protected function buildApplicationSearchGroupClause(
AphrontDatabaseConnection $conn) {
if ($this->getApplicationSearchMayJoinMultipleRows()) {
return qsprintf(
$conn,
'GROUP BY %Q',
$this->getApplicationSearchObjectPHIDColumn($conn));
} else {
return qsprintf($conn, '');
}
}
/**
* Construct a JOIN clause appropriate for applying ApplicationSearch
* constraints.
*
* @param AphrontDatabaseConnection Connection executing the query.
* @return string Join clause.
* @task appsearch
*/
protected function buildApplicationSearchJoinClause(
AphrontDatabaseConnection $conn) {
$joins = array();
foreach ($this->applicationSearchConstraints as $key => $constraint) {
$table = $constraint['table'];
$alias = $constraint['alias'];
$index = $constraint['index'];
$cond = $constraint['cond'];
$phid_column = $this->getApplicationSearchObjectPHIDColumn($conn);
switch ($cond) {
case '=':
// Figure out whether we need to do a LEFT JOIN or not. We need to
// LEFT JOIN if we're going to select "IS NULL" rows.
$join_type = qsprintf($conn, 'JOIN');
foreach ($constraint['constraints'] as $query_constraint) {
$op = $query_constraint->getOperator();
if ($op === PhabricatorQueryConstraint::OPERATOR_NULL) {
$join_type = qsprintf($conn, 'LEFT JOIN');
break;
}
}
$joins[] = qsprintf(
$conn,
'%Q %T %T ON %T.objectPHID = %Q
AND %T.indexKey = %s',
$join_type,
$table,
$alias,
$alias,
$phid_column,
$alias,
$index);
break;
case 'range':
list($min, $max) = $constraint['value'];
if (($min === null) && ($max === null)) {
// If there's no actual range constraint, just move on.
break;
}
if ($min === null) {
$constraint_clause = qsprintf(
$conn,
'%T.indexValue <= %d',
$alias,
$max);
} else if ($max === null) {
$constraint_clause = qsprintf(
$conn,
'%T.indexValue >= %d',
$alias,
$min);
} else {
$constraint_clause = qsprintf(
$conn,
'%T.indexValue BETWEEN %d AND %d',
$alias,
$min,
$max);
}
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.objectPHID = %Q
AND %T.indexKey = %s
AND (%Q)',
$table,
$alias,
$alias,
$phid_column,
$alias,
$index,
$constraint_clause);
break;
default:
throw new Exception(pht('Unknown constraint condition "%s"!', $cond));
}
}
$phid_column = $this->getApplicationSearchObjectPHIDColumn($conn);
$orderable = $this->getOrderableColumns();
$vector = $this->getOrderVector();
foreach ($vector as $order) {
$spec = $orderable[$order->getOrderKey()];
if (empty($spec['customfield'])) {
continue;
}
$table = $spec['customfield.index.table'];
$alias = $spec['table'];
$key = $spec['customfield.index.key'];
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T %T ON %T.objectPHID = %Q
AND %T.indexKey = %s',
$table,
$alias,
$alias,
$phid_column,
$alias,
$key);
}
if ($joins) {
return qsprintf($conn, '%LJ', $joins);
} else {
return qsprintf($conn, '');
}
}
/**
* Construct a WHERE clause appropriate for applying ApplicationSearch
* constraints.
*
* @param AphrontDatabaseConnection Connection executing the query.
* @return list<string> Where clause parts.
* @task appsearch
*/
protected function buildApplicationSearchWhereClause(
AphrontDatabaseConnection $conn) {
$where = array();
foreach ($this->applicationSearchConstraints as $key => $constraint) {
$alias = $constraint['alias'];
$cond = $constraint['cond'];
$type = $constraint['type'];
$data_values = $constraint['data'];
$constraint_values = $constraint['constraints'];
$constraint_parts = array();
switch ($cond) {
case '=':
if ($data_values) {
switch ($type) {
case 'string':
$constraint_parts[] = qsprintf(
$conn,
'%T.indexValue IN (%Ls)',
$alias,
$data_values);
break;
case 'int':
$constraint_parts[] = qsprintf(
$conn,
'%T.indexValue IN (%Ld)',
$alias,
$data_values);
break;
default:
throw new Exception(pht('Unknown index type "%s"!', $type));
}
}
if ($constraint_values) {
foreach ($constraint_values as $value) {
$op = $value->getOperator();
switch ($op) {
case PhabricatorQueryConstraint::OPERATOR_NULL:
$constraint_parts[] = qsprintf(
$conn,
'%T.indexValue IS NULL',
$alias);
break;
case PhabricatorQueryConstraint::OPERATOR_ANY:
$constraint_parts[] = qsprintf(
$conn,
'%T.indexValue IS NOT NULL',
$alias);
break;
default:
throw new Exception(
pht(
'No support for applying operator "%s" against '.
'index of type "%s".',
$op,
$type));
}
}
}
if ($constraint_parts) {
$where[] = qsprintf($conn, '%LO', $constraint_parts);
}
break;
}
}
return $where;
}
/* -( Integration with CustomField )--------------------------------------- */
/**
* @task customfield
*/
protected function getPagingValueMapForCustomFields(
PhabricatorCustomFieldInterface $object) {
// We have to get the current field values on the cursor object.
$fields = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
$fields->setViewer($this->getViewer());
$fields->readFieldsFromStorage($object);
$map = array();
foreach ($fields->getFields() as $field) {
$map['custom:'.$field->getFieldKey()] = $field->getValueForStorage();
}
return $map;
}
/**
* @task customfield
*/
protected function isCustomFieldOrderKey($key) {
$prefix = 'custom:';
return !strncmp($key, $prefix, strlen($prefix));
}
/* -( Ferret )------------------------------------------------------------- */
public function supportsFerretEngine() {
$object = $this->newResultObject();
return ($object instanceof PhabricatorFerretInterface);
}
public function withFerretQuery(
PhabricatorFerretEngine $engine,
PhabricatorSavedQuery $query) {
if (!$this->supportsFerretEngine()) {
throw new Exception(
pht(
'Query ("%s") does not support the Ferret fulltext engine.',
get_class($this)));
}
$this->ferretEngine = $engine;
$this->ferretQuery = $query;
return $this;
}
public function getFerretTokens() {
if (!$this->supportsFerretEngine()) {
throw new Exception(
pht(
'Query ("%s") does not support the Ferret fulltext engine.',
get_class($this)));
}
return $this->ferretTokens;
}
public function withFerretConstraint(
PhabricatorFerretEngine $engine,
array $fulltext_tokens) {
if (!$this->supportsFerretEngine()) {
throw new Exception(
pht(
'Query ("%s") does not support the Ferret fulltext engine.',
get_class($this)));
}
if ($this->ferretEngine) {
throw new Exception(
pht(
'Query may not have multiple fulltext constraints.'));
}
if (!$fulltext_tokens) {
return $this;
}
$this->ferretEngine = $engine;
$this->ferretTokens = $fulltext_tokens;
$current_function = $engine->getDefaultFunctionKey();
$table_map = array();
$idx = 1;
foreach ($this->ferretTokens as $fulltext_token) {
$raw_token = $fulltext_token->getToken();
$function = $raw_token->getFunction();
if ($function === null) {
$function = $current_function;
}
$raw_field = $engine->getFieldForFunction($function);
if (!isset($table_map[$function])) {
$alias = 'ftfield_'.$idx++;
$table_map[$function] = array(
'alias' => $alias,
'key' => $raw_field,
);
}
$current_function = $function;
}
// Join the title field separately so we can rank results.
$table_map['rank'] = array(
'alias' => 'ft_rank',
'key' => PhabricatorSearchDocumentFieldType::FIELD_TITLE,
);
$this->ferretTables = $table_map;
return $this;
}
protected function buildFerretSelectClause(AphrontDatabaseConnection $conn) {
$select = array();
if (!$this->supportsFerretEngine()) {
return $select;
}
- $vector = $this->getOrderVector();
- if (!$vector->containsKey('rank')) {
- // We only need to SELECT the virtual "_ft_rank" column if we're
+ if (!$this->hasFerretOrder()) {
+ // We only need to SELECT the virtual rank/relevance columns if we're
// actually sorting the results by rank.
return $select;
}
if (!$this->ferretEngine) {
- $select[] = qsprintf($conn, '0 _ft_rank');
+ $select[] = qsprintf($conn, '0 AS %T', self::FULLTEXT_RANK);
+ $select[] = qsprintf($conn, '0 AS %T', self::FULLTEXT_CREATED);
+ $select[] = qsprintf($conn, '0 AS %T', self::FULLTEXT_MODIFIED);
return $select;
}
$engine = $this->ferretEngine;
$stemmer = $engine->newStemmer();
$op_sub = PhutilSearchQueryCompiler::OPERATOR_SUBSTRING;
$op_not = PhutilSearchQueryCompiler::OPERATOR_NOT;
$table_alias = 'ft_rank';
$parts = array();
foreach ($this->ferretTokens as $fulltext_token) {
$raw_token = $fulltext_token->getToken();
$value = $raw_token->getValue();
if ($raw_token->getOperator() == $op_not) {
// Ignore "not" terms when ranking, since they aren't useful.
continue;
}
if ($raw_token->getOperator() == $op_sub) {
$is_substring = true;
} else {
$is_substring = false;
}
if ($is_substring) {
$parts[] = qsprintf(
$conn,
'IF(%T.rawCorpus LIKE %~, 2, 0)',
$table_alias,
$value);
continue;
}
if ($raw_token->isQuoted()) {
$is_quoted = true;
$is_stemmed = false;
} else {
$is_quoted = false;
$is_stemmed = true;
}
$term_constraints = array();
$term_value = $engine->newTermsCorpus($value);
$parts[] = qsprintf(
$conn,
'IF(%T.termCorpus LIKE %~, 2, 0)',
$table_alias,
$term_value);
if ($is_stemmed) {
$stem_value = $stemmer->stemToken($value);
$stem_value = $engine->newTermsCorpus($stem_value);
$parts[] = qsprintf(
$conn,
'IF(%T.normalCorpus LIKE %~, 1, 0)',
$table_alias,
$stem_value);
}
}
$parts[] = qsprintf($conn, '%d', 0);
$sum = array_shift($parts);
foreach ($parts as $part) {
$sum = qsprintf(
$conn,
'%Q + %Q',
$sum,
$part);
}
$select[] = qsprintf(
$conn,
- '%Q _ft_rank',
- $sum);
+ '%Q AS %T',
+ $sum,
+ self::FULLTEXT_RANK);
+
+ // See D20297. We select these as real columns in the result set so that
+ // constructions like this will work:
+ //
+ // ((SELECT ...) UNION (SELECT ...)) ORDER BY ...
+ //
+ // If the columns aren't part of the result set, the final "ORDER BY" can
+ // not act on them.
+
+ $select[] = qsprintf(
+ $conn,
+ 'ft_doc.epochCreated AS %T',
+ self::FULLTEXT_CREATED);
+
+ $select[] = qsprintf(
+ $conn,
+ 'ft_doc.epochModified AS %T',
+ self::FULLTEXT_MODIFIED);
return $select;
}
protected function buildFerretJoinClause(AphrontDatabaseConnection $conn) {
if (!$this->ferretEngine) {
return array();
}
$op_sub = PhutilSearchQueryCompiler::OPERATOR_SUBSTRING;
$op_not = PhutilSearchQueryCompiler::OPERATOR_NOT;
$engine = $this->ferretEngine;
$stemmer = $engine->newStemmer();
$ngram_table = $engine->getNgramsTableName();
$flat = array();
foreach ($this->ferretTokens as $fulltext_token) {
$raw_token = $fulltext_token->getToken();
// If this is a negated term like "-pomegranate", don't join the ngram
// table since we aren't looking for documents with this term. (We could
// LEFT JOIN the table and require a NULL row, but this is probably more
// trouble than it's worth.)
if ($raw_token->getOperator() == $op_not) {
continue;
}
$value = $raw_token->getValue();
$length = count(phutil_utf8v($value));
if ($raw_token->getOperator() == $op_sub) {
$is_substring = true;
} else {
$is_substring = false;
}
// If the user specified a substring query for a substring which is
// shorter than the ngram length, we can't use the ngram index, so
// don't do a join. We'll fall back to just doing LIKE on the full
// corpus.
if ($is_substring) {
if ($length < 3) {
continue;
}
}
if ($raw_token->isQuoted()) {
$is_stemmed = false;
} else {
$is_stemmed = true;
}
if ($is_substring) {
$ngrams = $engine->getSubstringNgramsFromString($value);
} else {
$terms_value = $engine->newTermsCorpus($value);
$ngrams = $engine->getTermNgramsFromString($terms_value);
// If this is a stemmed term, only look for ngrams present in both the
// unstemmed and stemmed variations.
if ($is_stemmed) {
// Trim the boundary space characters so the stemmer recognizes this
// is (or, at least, may be) a normal word and activates.
$terms_value = trim($terms_value, ' ');
$stem_value = $stemmer->stemToken($terms_value);
$stem_ngrams = $engine->getTermNgramsFromString($stem_value);
$ngrams = array_intersect($ngrams, $stem_ngrams);
}
}
foreach ($ngrams as $ngram) {
$flat[] = array(
'table' => $ngram_table,
'ngram' => $ngram,
);
}
}
// Remove common ngrams, like "the", which occur too frequently in
// documents to be useful in constraining the query. The best ngrams
// are obscure sequences which occur in very few documents.
if ($flat) {
$common_ngrams = queryfx_all(
$conn,
'SELECT ngram FROM %T WHERE ngram IN (%Ls)',
$engine->getCommonNgramsTableName(),
ipull($flat, 'ngram'));
$common_ngrams = ipull($common_ngrams, 'ngram', 'ngram');
foreach ($flat as $key => $spec) {
$ngram = $spec['ngram'];
if (isset($common_ngrams[$ngram])) {
unset($flat[$key]);
continue;
}
// NOTE: MySQL discards trailing whitespace in CHAR(X) columns.
$trim_ngram = rtrim($ngram, ' ');
if (isset($common_ngrams[$trim_ngram])) {
unset($flat[$key]);
continue;
}
}
}
// MySQL only allows us to join a maximum of 61 tables per query. Each
// ngram is going to cost us a join toward that limit, so if the user
// specified a very long query string, just pick 16 of the ngrams
// at random.
if (count($flat) > 16) {
shuffle($flat);
$flat = array_slice($flat, 0, 16);
}
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$phid_column = qsprintf($conn, '%T.%T', $alias, 'phid');
} else {
$phid_column = qsprintf($conn, '%T', 'phid');
}
$document_table = $engine->getDocumentTableName();
$field_table = $engine->getFieldTableName();
$joins = array();
$joins[] = qsprintf(
$conn,
'JOIN %T ft_doc ON ft_doc.objectPHID = %Q',
$document_table,
$phid_column);
$idx = 1;
foreach ($flat as $spec) {
$table = $spec['table'];
$ngram = $spec['ngram'];
$alias = 'ftngram_'.$idx++;
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.documentID = ft_doc.id AND %T.ngram = %s',
$table,
$alias,
$alias,
$alias,
$ngram);
}
foreach ($this->ferretTables as $table) {
$alias = $table['alias'];
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON ft_doc.id = %T.documentID
AND %T.fieldKey = %s',
$field_table,
$alias,
$alias,
$alias,
$table['key']);
}
return $joins;
}
protected function buildFerretWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->ferretEngine) {
return array();
}
$engine = $this->ferretEngine;
$stemmer = $engine->newStemmer();
$table_map = $this->ferretTables;
$op_sub = PhutilSearchQueryCompiler::OPERATOR_SUBSTRING;
$op_not = PhutilSearchQueryCompiler::OPERATOR_NOT;
$op_exact = PhutilSearchQueryCompiler::OPERATOR_EXACT;
$where = array();
$current_function = 'all';
foreach ($this->ferretTokens as $fulltext_token) {
$raw_token = $fulltext_token->getToken();
$value = $raw_token->getValue();
$function = $raw_token->getFunction();
if ($function === null) {
$function = $current_function;
}
$current_function = $function;
$table_alias = $table_map[$function]['alias'];
$is_not = ($raw_token->getOperator() == $op_not);
if ($raw_token->getOperator() == $op_sub) {
$is_substring = true;
} else {
$is_substring = false;
}
// If we're doing exact search, just test the raw corpus.
$is_exact = ($raw_token->getOperator() == $op_exact);
if ($is_exact) {
if ($is_not) {
$where[] = qsprintf(
$conn,
'(%T.rawCorpus != %s)',
$table_alias,
$value);
} else {
$where[] = qsprintf(
$conn,
'(%T.rawCorpus = %s)',
$table_alias,
$value);
}
continue;
}
// If we're doing substring search, we just match against the raw corpus
// and we're done.
if ($is_substring) {
if ($is_not) {
$where[] = qsprintf(
$conn,
'(%T.rawCorpus NOT LIKE %~)',
$table_alias,
$value);
} else {
$where[] = qsprintf(
$conn,
'(%T.rawCorpus LIKE %~)',
$table_alias,
$value);
}
continue;
}
// Otherwise, we need to match against the term corpus and the normal
// corpus, so that searching for "raw" does not find "strawberry".
if ($raw_token->isQuoted()) {
$is_quoted = true;
$is_stemmed = false;
} else {
$is_quoted = false;
$is_stemmed = true;
}
// Never stem negated queries, since this can exclude results users
// did not mean to exclude and generally confuse things.
if ($is_not) {
$is_stemmed = false;
}
$term_constraints = array();
$term_value = $engine->newTermsCorpus($value);
if ($is_not) {
$term_constraints[] = qsprintf(
$conn,
'(%T.termCorpus NOT LIKE %~)',
$table_alias,
$term_value);
} else {
$term_constraints[] = qsprintf(
$conn,
'(%T.termCorpus LIKE %~)',
$table_alias,
$term_value);
}
if ($is_stemmed) {
$stem_value = $stemmer->stemToken($value);
$stem_value = $engine->newTermsCorpus($stem_value);
$term_constraints[] = qsprintf(
$conn,
'(%T.normalCorpus LIKE %~)',
$table_alias,
$stem_value);
}
if ($is_not) {
$where[] = qsprintf(
$conn,
'%LA',
$term_constraints);
} else if ($is_quoted) {
$where[] = qsprintf(
$conn,
'(%T.rawCorpus LIKE %~ AND %LO)',
$table_alias,
$value,
$term_constraints);
} else {
$where[] = qsprintf(
$conn,
'%LO',
$term_constraints);
}
}
if ($this->ferretQuery) {
$query = $this->ferretQuery;
$author_phids = $query->getParameter('authorPHIDs');
if ($author_phids) {
$where[] = qsprintf(
$conn,
'ft_doc.authorPHID IN (%Ls)',
$author_phids);
}
$with_unowned = $query->getParameter('withUnowned');
$with_any = $query->getParameter('withAnyOwner');
if ($with_any && $with_unowned) {
throw new PhabricatorEmptyQueryException(
pht(
'This query matches only unowned documents owned by anyone, '.
'which is impossible.'));
}
$owner_phids = $query->getParameter('ownerPHIDs');
if ($owner_phids && !$with_any) {
if ($with_unowned) {
$where[] = qsprintf(
$conn,
'ft_doc.ownerPHID IN (%Ls) OR ft_doc.ownerPHID IS NULL',
$owner_phids);
} else {
$where[] = qsprintf(
$conn,
'ft_doc.ownerPHID IN (%Ls)',
$owner_phids);
}
} else if ($with_unowned) {
$where[] = qsprintf(
$conn,
'ft_doc.ownerPHID IS NULL');
}
if ($with_any) {
$where[] = qsprintf(
$conn,
'ft_doc.ownerPHID IS NOT NULL');
}
$rel_open = PhabricatorSearchRelationship::RELATIONSHIP_OPEN;
$statuses = $query->getParameter('statuses');
$is_closed = null;
if ($statuses) {
$statuses = array_fuse($statuses);
if (count($statuses) == 1) {
if (isset($statuses[$rel_open])) {
$is_closed = 0;
} else {
$is_closed = 1;
}
}
}
if ($is_closed !== null) {
$where[] = qsprintf(
$conn,
'ft_doc.isClosed = %d',
$is_closed);
}
}
return $where;
}
protected function shouldGroupFerretResultRows() {
return (bool)$this->ferretTokens;
}
/* -( Ngrams )------------------------------------------------------------- */
protected function withNgramsConstraint(
PhabricatorSearchNgrams $index,
$value) {
if (strlen($value)) {
$this->ngrams[] = array(
'index' => $index,
'value' => $value,
'length' => count(phutil_utf8v($value)),
);
}
return $this;
}
protected function buildNgramsJoinClause(AphrontDatabaseConnection $conn) {
$flat = array();
foreach ($this->ngrams as $spec) {
$index = $spec['index'];
$value = $spec['value'];
$length = $spec['length'];
if ($length >= 3) {
$ngrams = $index->getNgramsFromString($value, 'query');
$prefix = false;
} else if ($length == 2) {
$ngrams = $index->getNgramsFromString($value, 'prefix');
$prefix = false;
} else {
$ngrams = array(' '.$value);
$prefix = true;
}
foreach ($ngrams as $ngram) {
$flat[] = array(
'table' => $index->getTableName(),
'ngram' => $ngram,
'prefix' => $prefix,
);
}
}
// MySQL only allows us to join a maximum of 61 tables per query. Each
// ngram is going to cost us a join toward that limit, so if the user
// specified a very long query string, just pick 16 of the ngrams
// at random.
if (count($flat) > 16) {
shuffle($flat);
$flat = array_slice($flat, 0, 16);
}
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$id_column = qsprintf($conn, '%T.%T', $alias, 'id');
} else {
$id_column = qsprintf($conn, '%T', 'id');
}
$idx = 1;
$joins = array();
foreach ($flat as $spec) {
$table = $spec['table'];
$ngram = $spec['ngram'];
$prefix = $spec['prefix'];
$alias = 'ngm'.$idx++;
if ($prefix) {
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.objectID = %Q AND %T.ngram LIKE %>',
$table,
$alias,
$alias,
$id_column,
$alias,
$ngram);
} else {
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.objectID = %Q AND %T.ngram = %s',
$table,
$alias,
$alias,
$id_column,
$alias,
$ngram);
}
}
return $joins;
}
protected function buildNgramsWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
foreach ($this->ngrams as $ngram) {
$index = $ngram['index'];
$value = $ngram['value'];
$column = $index->getColumnName();
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$column = qsprintf($conn, '%T.%T', $alias, $column);
} else {
$column = qsprintf($conn, '%T', $column);
}
$tokens = $index->tokenizeString($value);
foreach ($tokens as $token) {
$where[] = qsprintf(
$conn,
'%Q LIKE %~',
$column,
$token);
}
}
return $where;
}
protected function shouldGroupNgramResultRows() {
return (bool)$this->ngrams;
}
/* -( Edge Logic )--------------------------------------------------------- */
/**
* Convenience method for specifying edge logic constraints with a list of
* PHIDs.
*
* @param const Edge constant.
* @param const Constraint operator.
* @param list<phid> List of PHIDs.
* @return this
* @task edgelogic
*/
public function withEdgeLogicPHIDs($edge_type, $operator, array $phids) {
$constraints = array();
foreach ($phids as $phid) {
$constraints[] = new PhabricatorQueryConstraint($operator, $phid);
}
return $this->withEdgeLogicConstraints($edge_type, $constraints);
}
/**
* @return this
* @task edgelogic
*/
public function withEdgeLogicConstraints($edge_type, array $constraints) {
assert_instances_of($constraints, 'PhabricatorQueryConstraint');
$constraints = mgroup($constraints, 'getOperator');
foreach ($constraints as $operator => $list) {
foreach ($list as $item) {
$this->edgeLogicConstraints[$edge_type][$operator][] = $item;
}
}
$this->edgeLogicConstraintsAreValid = false;
return $this;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicSelectClause(AphrontDatabaseConnection $conn) {
$select = array();
$this->validateEdgeLogicConstraints();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_AND:
if (count($list) > 1) {
$select[] = qsprintf(
$conn,
'COUNT(DISTINCT(%T.dst)) %T',
$alias,
$this->buildEdgeLogicTableAliasCount($alias));
}
break;
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
// This is tricky. We have a query which specifies multiple
// projects, each of which may have an arbitrarily large number
// of descendants.
// Suppose the projects are "Engineering" and "Operations", and
// "Engineering" has subprojects X, Y and Z.
// We first use `FIELD(dst, X, Y, Z)` to produce a 0 if a row
// is not part of Engineering at all, or some number other than
// 0 if it is.
// Then we use `IF(..., idx, NULL)` to convert the 0 to a NULL and
// any other value to an index (say, 1) for the ancestor.
// We build these up for every ancestor, then use `COALESCE(...)`
// to select the non-null one, giving us an ancestor which this
// row is a member of.
// From there, we use `COUNT(DISTINCT(...))` to make sure that
// each result row is a member of all ancestors.
if (count($list) > 1) {
$idx = 1;
$parts = array();
foreach ($list as $constraint) {
$parts[] = qsprintf(
$conn,
'IF(FIELD(%T.dst, %Ls) != 0, %d, NULL)',
$alias,
(array)$constraint->getValue(),
$idx++);
}
$parts = qsprintf($conn, '%LQ', $parts);
$select[] = qsprintf(
$conn,
'COUNT(DISTINCT(COALESCE(%Q))) %T',
$parts,
$this->buildEdgeLogicTableAliasAncestor($alias));
}
break;
default:
break;
}
}
}
return $select;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicJoinClause(AphrontDatabaseConnection $conn) {
$edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE;
$phid_column = $this->getApplicationSearchObjectPHIDColumn($conn);
$joins = array();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
$op_null = PhabricatorQueryConstraint::OPERATOR_NULL;
$has_null = isset($constraints[$op_null]);
// If we're going to process an only() operator, build a list of the
// acceptable set of PHIDs first. We'll only match results which have
// no edges to any other PHIDs.
$all_phids = array();
if (isset($constraints[PhabricatorQueryConstraint::OPERATOR_ONLY])) {
foreach ($constraints as $operator => $list) {
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
case PhabricatorQueryConstraint::OPERATOR_AND:
case PhabricatorQueryConstraint::OPERATOR_OR:
foreach ($list as $constraint) {
$value = (array)$constraint->getValue();
foreach ($value as $v) {
$all_phids[$v] = $v;
}
}
break;
}
}
}
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
$phids = array();
foreach ($list as $constraint) {
$value = (array)$constraint->getValue();
foreach ($value as $v) {
$phids[$v] = $v;
}
}
$phids = array_keys($phids);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_NOT:
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T %T ON %Q = %T.src AND %T.type = %d
AND %T.dst IN (%Ls)',
$edge_table,
$alias,
$phid_column,
$alias,
$alias,
$type,
$alias,
$phids);
break;
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
case PhabricatorQueryConstraint::OPERATOR_AND:
case PhabricatorQueryConstraint::OPERATOR_OR:
// If we're including results with no matches, we have to degrade
// this to a LEFT join. We'll use WHERE to select matching rows
// later.
if ($has_null) {
$join_type = qsprintf($conn, 'LEFT');
} else {
$join_type = qsprintf($conn, '');
}
$joins[] = qsprintf(
$conn,
'%Q JOIN %T %T ON %Q = %T.src AND %T.type = %d
AND %T.dst IN (%Ls)',
$join_type,
$edge_table,
$alias,
$phid_column,
$alias,
$alias,
$type,
$alias,
$phids);
break;
case PhabricatorQueryConstraint::OPERATOR_NULL:
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T %T ON %Q = %T.src AND %T.type = %d',
$edge_table,
$alias,
$phid_column,
$alias,
$alias,
$type);
break;
case PhabricatorQueryConstraint::OPERATOR_ONLY:
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T %T ON %Q = %T.src AND %T.type = %d
AND %T.dst NOT IN (%Ls)',
$edge_table,
$alias,
$phid_column,
$alias,
$alias,
$type,
$alias,
$all_phids);
break;
}
}
}
return $joins;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
$full = array();
$null = array();
$op_null = PhabricatorQueryConstraint::OPERATOR_NULL;
$has_null = isset($constraints[$op_null]);
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_NOT:
case PhabricatorQueryConstraint::OPERATOR_ONLY:
$full[] = qsprintf(
$conn,
'%T.dst IS NULL',
$alias);
break;
case PhabricatorQueryConstraint::OPERATOR_AND:
case PhabricatorQueryConstraint::OPERATOR_OR:
if ($has_null) {
$full[] = qsprintf(
$conn,
'%T.dst IS NOT NULL',
$alias);
}
break;
case PhabricatorQueryConstraint::OPERATOR_NULL:
$null[] = qsprintf(
$conn,
'%T.dst IS NULL',
$alias);
break;
}
}
if ($full && $null) {
$where[] = qsprintf($conn, '(%LA OR %LA)', $full, $null);
} else if ($full) {
foreach ($full as $condition) {
$where[] = $condition;
}
} else if ($null) {
foreach ($null as $condition) {
$where[] = $condition;
}
}
}
return $where;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicHavingClause(AphrontDatabaseConnection $conn) {
$having = array();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_AND:
if (count($list) > 1) {
$having[] = qsprintf(
$conn,
'%T = %d',
$this->buildEdgeLogicTableAliasCount($alias),
count($list));
}
break;
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
if (count($list) > 1) {
$having[] = qsprintf(
$conn,
'%T = %d',
$this->buildEdgeLogicTableAliasAncestor($alias),
count($list));
}
break;
}
}
}
return $having;
}
/**
* @task edgelogic
*/
public function shouldGroupEdgeLogicResultRows() {
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_NOT:
case PhabricatorQueryConstraint::OPERATOR_AND:
case PhabricatorQueryConstraint::OPERATOR_OR:
if (count($list) > 1) {
return true;
}
break;
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
// NOTE: We must always group query results rows when using an
// "ANCESTOR" operator because a single task may be related to
// two different descendants of a particular ancestor. For
// discussion, see T12753.
return true;
case PhabricatorQueryConstraint::OPERATOR_NULL:
case PhabricatorQueryConstraint::OPERATOR_ONLY:
return true;
}
}
}
return false;
}
/**
* @task edgelogic
*/
private function getEdgeLogicTableAlias($operator, $type) {
return 'edgelogic_'.$operator.'_'.$type;
}
/**
* @task edgelogic
*/
private function buildEdgeLogicTableAliasCount($alias) {
return $alias.'_count';
}
/**
* @task edgelogic
*/
private function buildEdgeLogicTableAliasAncestor($alias) {
return $alias.'_ancestor';
}
/**
* Select certain edge logic constraint values.
*
* @task edgelogic
*/
protected function getEdgeLogicValues(
array $edge_types,
array $operators) {
$values = array();
$constraint_lists = $this->edgeLogicConstraints;
if ($edge_types) {
$constraint_lists = array_select_keys($constraint_lists, $edge_types);
}
foreach ($constraint_lists as $type => $constraints) {
if ($operators) {
$constraints = array_select_keys($constraints, $operators);
}
foreach ($constraints as $operator => $list) {
foreach ($list as $constraint) {
$value = (array)$constraint->getValue();
foreach ($value as $v) {
$values[] = $v;
}
}
}
}
return $values;
}
/**
* Validate edge logic constraints for the query.
*
* @return this
* @task edgelogic
*/
private function validateEdgeLogicConstraints() {
if ($this->edgeLogicConstraintsAreValid) {
return $this;
}
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_EMPTY:
throw new PhabricatorEmptyQueryException(
pht('This query specifies an empty constraint.'));
}
}
}
// This should probably be more modular, eventually, but we only do
// project-based edge logic today.
$project_phids = $this->getEdgeLogicValues(
array(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
),
array(
PhabricatorQueryConstraint::OPERATOR_AND,
PhabricatorQueryConstraint::OPERATOR_OR,
PhabricatorQueryConstraint::OPERATOR_NOT,
PhabricatorQueryConstraint::OPERATOR_ANCESTOR,
));
if ($project_phids) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs($project_phids)
->execute();
$projects = mpull($projects, null, 'getPHID');
foreach ($project_phids as $phid) {
if (empty($projects[$phid])) {
throw new PhabricatorEmptyQueryException(
pht(
'This query is constrained by a project you do not have '.
'permission to see.'));
}
}
}
$op_and = PhabricatorQueryConstraint::OPERATOR_AND;
$op_or = PhabricatorQueryConstraint::OPERATOR_OR;
$op_ancestor = PhabricatorQueryConstraint::OPERATOR_ANCESTOR;
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_ONLY:
if (count($list) > 1) {
throw new PhabricatorEmptyQueryException(
pht(
'This query specifies only() more than once.'));
}
$have_and = idx($constraints, $op_and);
$have_or = idx($constraints, $op_or);
$have_ancestor = idx($constraints, $op_ancestor);
if (!$have_and && !$have_or && !$have_ancestor) {
throw new PhabricatorEmptyQueryException(
pht(
'This query specifies only(), but no other constraints '.
'which it can apply to.'));
}
break;
}
}
}
$this->edgeLogicConstraintsAreValid = true;
return $this;
}
/* -( Spaces )------------------------------------------------------------- */
/**
* Constrain the query to return results from only specific Spaces.
*
* Pass a list of Space PHIDs, or `null` to represent the default space. Only
* results in those Spaces will be returned.
*
* Queries are always constrained to include only results from spaces the
* viewer has access to.
*
* @param list<phid|null>
* @task spaces
*/
public function withSpacePHIDs(array $space_phids) {
$object = $this->newResultObject();
if (!$object) {
throw new Exception(
pht(
'This query (of class "%s") does not implement newResultObject(), '.
'but must implement this method to enable support for Spaces.',
get_class($this)));
}
if (!($object instanceof PhabricatorSpacesInterface)) {
throw new Exception(
pht(
'This query (of class "%s") returned an object of class "%s" from '.
'getNewResultObject(), but it does not implement the required '.
'interface ("%s"). Objects must implement this interface to enable '.
'Spaces support.',
get_class($this),
get_class($object),
'PhabricatorSpacesInterface'));
}
$this->spacePHIDs = $space_phids;
return $this;
}
public function withSpaceIsArchived($archived) {
$this->spaceIsArchived = $archived;
return $this;
}
/**
* Constrain the query to include only results in valid Spaces.
*
* This method builds part of a WHERE clause which considers the spaces the
* viewer has access to see with any explicit constraint on spaces added by
* @{method:withSpacePHIDs}.
*
* @param AphrontDatabaseConnection Database connection.
* @return string Part of a WHERE clause.
* @task spaces
*/
private function buildSpacesWhereClause(AphrontDatabaseConnection $conn) {
$object = $this->newResultObject();
if (!$object) {
return null;
}
if (!($object instanceof PhabricatorSpacesInterface)) {
return null;
}
$viewer = $this->getViewer();
// If we have an omnipotent viewer and no formal space constraints, don't
// emit a clause. This primarily enables older migrations to run cleanly,
// without fataling because they try to match a `spacePHID` column which
// does not exist yet. See T8743, T8746.
if ($viewer->isOmnipotent()) {
if ($this->spaceIsArchived === null && $this->spacePHIDs === null) {
return null;
}
}
// See T13240. If this query raises policy exceptions, don't filter objects
// in the MySQL layer. We want them to reach the application layer so we
// can reject them and raise an exception.
if ($this->shouldRaisePolicyExceptions()) {
return null;
}
$space_phids = array();
$include_null = false;
$all = PhabricatorSpacesNamespaceQuery::getAllSpaces();
if (!$all) {
// If there are no spaces at all, implicitly give the viewer access to
// the default space.
$include_null = true;
} else {
// Otherwise, give them access to the spaces they have permission to
// see.
$viewer_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces(
$viewer);
foreach ($viewer_spaces as $viewer_space) {
if ($this->spaceIsArchived !== null) {
if ($viewer_space->getIsArchived() != $this->spaceIsArchived) {
continue;
}
}
$phid = $viewer_space->getPHID();
$space_phids[$phid] = $phid;
if ($viewer_space->getIsDefaultNamespace()) {
$include_null = true;
}
}
}
// If we have additional explicit constraints, evaluate them now.
if ($this->spacePHIDs !== null) {
$explicit = array();
$explicit_null = false;
foreach ($this->spacePHIDs as $phid) {
if ($phid === null) {
$space = PhabricatorSpacesNamespaceQuery::getDefaultSpace();
} else {
$space = idx($all, $phid);
}
if ($space) {
$phid = $space->getPHID();
$explicit[$phid] = $phid;
if ($space->getIsDefaultNamespace()) {
$explicit_null = true;
}
}
}
// If the viewer can see the default space but it isn't on the explicit
// list of spaces to query, don't match it.
if ($include_null && !$explicit_null) {
$include_null = false;
}
// Include only the spaces common to the viewer and the constraints.
$space_phids = array_intersect_key($space_phids, $explicit);
}
if (!$space_phids && !$include_null) {
if ($this->spacePHIDs === null) {
throw new PhabricatorEmptyQueryException(
pht('You do not have access to any spaces.'));
} else {
throw new PhabricatorEmptyQueryException(
pht(
'You do not have access to any of the spaces this query '.
'is constrained to.'));
}
}
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$col = qsprintf($conn, '%T.spacePHID', $alias);
} else {
$col = qsprintf($conn, 'spacePHID');
}
if ($space_phids && $include_null) {
return qsprintf(
$conn,
'(%Q IN (%Ls) OR %Q IS NULL)',
$col,
$space_phids,
$col);
} else if ($space_phids) {
return qsprintf(
$conn,
'%Q IN (%Ls)',
$col,
$space_phids);
} else {
return qsprintf(
$conn,
'%Q IS NULL',
$col);
}
}
+ private function hasFerretOrder() {
+ $vector = $this->getOrderVector();
+
+ if ($vector->containsKey('rank')) {
+ return true;
+ }
+
+ if ($vector->containsKey('fulltext-created')) {
+ return true;
+ }
+
+ if ($vector->containsKey('fulltext-modified')) {
+ return true;
+ }
+
+ return false;
+ }
+
}
diff --git a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
index 7aa0f28df..8780584f9 100644
--- a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
+++ b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
@@ -1,751 +1,781 @@
<?php
/**
* A @{class:PhabricatorQuery} which filters results according to visibility
* policies for the querying user. Broadly, this class allows you to implement
* a query that returns only objects the user is allowed to see.
*
* $results = id(new ExampleQuery())
* ->setViewer($user)
* ->withConstraint($example)
* ->execute();
*
* Normally, you should extend @{class:PhabricatorCursorPagedPolicyAwareQuery},
* not this class. @{class:PhabricatorCursorPagedPolicyAwareQuery} provides a
* more practical interface for building usable queries against most object
* types.
*
* NOTE: Although this class extends @{class:PhabricatorOffsetPagedQuery},
* offset paging with policy filtering is not efficient. All results must be
* loaded into the application and filtered here: skipping `N` rows via offset
* is an `O(N)` operation with a large constant. Prefer cursor-based paging
* with @{class:PhabricatorCursorPagedPolicyAwareQuery}, which can filter far
* more efficiently in MySQL.
*
* @task config Query Configuration
* @task exec Executing Queries
* @task policyimpl Policy Query Implementation
*/
abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery {
private $viewer;
private $parentQuery;
private $rawResultLimit;
private $capabilities;
private $workspace = array();
private $inFlightPHIDs = array();
private $policyFilteredPHIDs = array();
/**
* Should we continue or throw an exception when a query result is filtered
* by policy rules?
*
* Values are `true` (raise exceptions), `false` (do not raise exceptions)
* and `null` (inherit from parent query, with no exceptions by default).
*/
private $raisePolicyExceptions;
private $isOverheated;
+ private $returnPartialResultsOnOverheat;
+ private $disableOverheating;
/* -( Query Configuration )------------------------------------------------ */
/**
* Set the viewer who is executing the query. Results will be filtered
* according to the viewer's capabilities. You must set a viewer to execute
* a policy query.
*
* @param PhabricatorUser The viewing user.
* @return this
* @task config
*/
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
/**
* Get the query's viewer.
*
* @return PhabricatorUser The viewing user.
* @task config
*/
final public function getViewer() {
return $this->viewer;
}
/**
* Set the parent query of this query. This is useful for nested queries so
* that configuration like whether or not to raise policy exceptions is
* seamlessly passed along to child queries.
*
* @return this
* @task config
*/
final public function setParentQuery(PhabricatorPolicyAwareQuery $query) {
$this->parentQuery = $query;
return $this;
}
/**
* Get the parent query. See @{method:setParentQuery} for discussion.
*
* @return PhabricatorPolicyAwareQuery The parent query.
* @task config
*/
final public function getParentQuery() {
return $this->parentQuery;
}
/**
* Hook to configure whether this query should raise policy exceptions.
*
* @return this
* @task config
*/
final public function setRaisePolicyExceptions($bool) {
$this->raisePolicyExceptions = $bool;
return $this;
}
/**
* @return bool
* @task config
*/
final public function shouldRaisePolicyExceptions() {
return (bool)$this->raisePolicyExceptions;
}
/**
* @task config
*/
final public function requireCapabilities(array $capabilities) {
$this->capabilities = $capabilities;
return $this;
}
+ final public function setReturnPartialResultsOnOverheat($bool) {
+ $this->returnPartialResultsOnOverheat = $bool;
+ return $this;
+ }
+
+ final public function setDisableOverheating($disable_overheating) {
+ $this->disableOverheating = $disable_overheating;
+ return $this;
+ }
+
/* -( Query Execution )---------------------------------------------------- */
/**
* Execute the query, expecting a single result. This method simplifies
* loading objects for detail pages or edit views.
*
* // Load one result by ID.
* $obj = id(new ExampleQuery())
* ->setViewer($user)
* ->withIDs(array($id))
* ->executeOne();
* if (!$obj) {
* return new Aphront404Response();
* }
*
* If zero results match the query, this method returns `null`.
* If one result matches the query, this method returns that result.
*
* If two or more results match the query, this method throws an exception.
* You should use this method only when the query constraints guarantee at
* most one match (e.g., selecting a specific ID or PHID).
*
* If one result matches the query but it is caught by the policy filter (for
* example, the user is trying to view or edit an object which exists but
* which they do not have permission to see) a policy exception is thrown.
*
* @return mixed Single result, or null.
* @task exec
*/
final public function executeOne() {
$this->setRaisePolicyExceptions(true);
try {
$results = $this->execute();
} catch (Exception $ex) {
$this->setRaisePolicyExceptions(false);
throw $ex;
}
if (count($results) > 1) {
throw new Exception(pht('Expected a single result!'));
}
if (!$results) {
return null;
}
return head($results);
}
/**
* Execute the query, loading all visible results.
*
* @return list<PhabricatorPolicyInterface> Result objects.
* @task exec
*/
final public function execute() {
if (!$this->viewer) {
throw new PhutilInvalidStateException('setViewer');
}
$parent_query = $this->getParentQuery();
if ($parent_query && ($this->raisePolicyExceptions === null)) {
$this->setRaisePolicyExceptions(
$parent_query->shouldRaisePolicyExceptions());
}
$results = array();
$filter = $this->getPolicyFilter();
$offset = (int)$this->getOffset();
$limit = (int)$this->getLimit();
$count = 0;
if ($limit) {
$need = $offset + $limit;
} else {
$need = 0;
}
$this->willExecute();
// If we examine and filter significantly more objects than the query
// limit, we stop early. This prevents us from looping through a huge
// number of records when the viewer can see few or none of them. See
// T11773 for some discussion.
$this->isOverheated = false;
$overheat_limit = $limit * 10;
$total_seen = 0;
do {
if ($need) {
$this->rawResultLimit = min($need - $count, 1024);
} else {
$this->rawResultLimit = 0;
}
if ($this->canViewerUseQueryApplication()) {
try {
$page = $this->loadPage();
} catch (PhabricatorEmptyQueryException $ex) {
$page = array();
}
} else {
$page = array();
}
$total_seen += count($page);
if ($page) {
$maybe_visible = $this->willFilterPage($page);
if ($maybe_visible) {
$maybe_visible = $this->applyWillFilterPageExtensions($maybe_visible);
}
} else {
$maybe_visible = array();
}
if ($this->shouldDisablePolicyFiltering()) {
$visible = $maybe_visible;
} else {
$visible = $filter->apply($maybe_visible);
$policy_filtered = array();
foreach ($maybe_visible as $key => $object) {
if (empty($visible[$key])) {
$phid = $object->getPHID();
if ($phid) {
$policy_filtered[$phid] = $phid;
}
}
}
$this->addPolicyFilteredPHIDs($policy_filtered);
}
if ($visible) {
$visible = $this->didFilterPage($visible);
}
$removed = array();
foreach ($maybe_visible as $key => $object) {
if (empty($visible[$key])) {
$removed[$key] = $object;
}
}
$this->didFilterResults($removed);
+ // NOTE: We call "nextPage()" before checking if we've found enough
+ // results because we want to build the internal cursor object even
+ // if we don't need to execute another query: the internal cursor may
+ // be used by a parent query that is using this query to translate an
+ // external cursor into an internal cursor.
+ $this->nextPage($page);
+
foreach ($visible as $key => $result) {
++$count;
// If we have an offset, we just ignore that many results and start
// storing them only once we've hit the offset. This reduces memory
// requirements for large offsets, compared to storing them all and
// slicing them away later.
if ($count > $offset) {
$results[$key] = $result;
}
if ($need && ($count >= $need)) {
// If we have all the rows we need, break out of the paging query.
break 2;
}
}
if (!$this->rawResultLimit) {
// If we don't have a load count, we loaded all the results. We do
// not need to load another page.
break;
}
if (count($page) < $this->rawResultLimit) {
// If we have a load count but the unfiltered results contained fewer
// objects, we know this was the last page of objects; we do not need
// to load another page because we can deduce it would be empty.
break;
}
- $this->nextPage($page);
+ if (!$this->disableOverheating) {
+ if ($overheat_limit && ($total_seen >= $overheat_limit)) {
+ $this->isOverheated = true;
+
+ if (!$this->returnPartialResultsOnOverheat) {
+ throw new Exception(
+ pht(
+ 'Query (of class "%s") overheated: examined more than %s '.
+ 'raw rows without finding %s visible objects.',
+ get_class($this),
+ new PhutilNumber($overheat_limit),
+ new PhutilNumber($need)));
+ }
- if ($overheat_limit && ($total_seen >= $overheat_limit)) {
- $this->isOverheated = true;
- break;
+ break;
+ }
}
} while (true);
$results = $this->didLoadResults($results);
return $results;
}
private function getPolicyFilter() {
$filter = new PhabricatorPolicyFilter();
$filter->setViewer($this->viewer);
$capabilities = $this->getRequiredCapabilities();
$filter->requireCapabilities($capabilities);
$filter->raisePolicyExceptions($this->shouldRaisePolicyExceptions());
return $filter;
}
protected function getRequiredCapabilities() {
if ($this->capabilities) {
return $this->capabilities;
}
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
protected function applyPolicyFilter(array $objects, array $capabilities) {
if ($this->shouldDisablePolicyFiltering()) {
return $objects;
}
$filter = $this->getPolicyFilter();
$filter->requireCapabilities($capabilities);
return $filter->apply($objects);
}
protected function didRejectResult(PhabricatorPolicyInterface $object) {
// Some objects (like commits) may be rejected because related objects
// (like repositories) can not be loaded. In some cases, we may need these
// related objects to determine the object policy, so it's expected that
// we may occasionally be unable to determine the policy.
try {
$policy = $object->getPolicy(PhabricatorPolicyCapability::CAN_VIEW);
} catch (Exception $ex) {
$policy = null;
}
// Mark this object as filtered so handles can render "Restricted" instead
// of "Unknown".
$phid = $object->getPHID();
$this->addPolicyFilteredPHIDs(array($phid => $phid));
$this->getPolicyFilter()->rejectObject(
$object,
$policy,
PhabricatorPolicyCapability::CAN_VIEW);
}
public function addPolicyFilteredPHIDs(array $phids) {
$this->policyFilteredPHIDs += $phids;
if ($this->getParentQuery()) {
$this->getParentQuery()->addPolicyFilteredPHIDs($phids);
}
return $this;
}
public function getIsOverheated() {
if ($this->isOverheated === null) {
throw new PhutilInvalidStateException('execute');
}
return $this->isOverheated;
}
/**
* Return a map of all object PHIDs which were loaded in the query but
* filtered out by policy constraints. This allows a caller to distinguish
* between objects which do not exist (or, at least, were filtered at the
* content level) and objects which exist but aren't visible.
*
* @return map<phid, phid> Map of object PHIDs which were filtered
* by policies.
* @task exec
*/
public function getPolicyFilteredPHIDs() {
return $this->policyFilteredPHIDs;
}
/* -( Query Workspace )---------------------------------------------------- */
/**
* Put a map of objects into the query workspace. Many queries perform
* subqueries, which can eventually end up loading the same objects more than
* once (often to perform policy checks).
*
* For example, loading a user may load the user's profile image, which might
* load the user object again in order to verify that the viewer has
* permission to see the file.
*
* The "query workspace" allows queries to load objects from elsewhere in a
* query block instead of refetching them.
*
* When using the query workspace, it's important to obey two rules:
*
* **Never put objects into the workspace which the viewer may not be able
* to see**. You need to apply all policy filtering //before// putting
* objects in the workspace. Otherwise, subqueries may read the objects and
* use them to permit access to content the user shouldn't be able to view.
*
* **Fully enrich objects pulled from the workspace.** After pulling objects
* from the workspace, you still need to load and attach any additional
* content the query requests. Otherwise, a query might return objects
* without requested content.
*
* Generally, you do not need to update the workspace yourself: it is
* automatically populated as a side effect of objects surviving policy
* filtering.
*
* @param map<phid, PhabricatorPolicyInterface> Objects to add to the query
* workspace.
* @return this
* @task workspace
*/
public function putObjectsInWorkspace(array $objects) {
$parent = $this->getParentQuery();
if ($parent) {
$parent->putObjectsInWorkspace($objects);
return $this;
}
assert_instances_of($objects, 'PhabricatorPolicyInterface');
$viewer_fragment = $this->getViewer()->getCacheFragment();
// The workspace is scoped per viewer to prevent accidental contamination.
if (empty($this->workspace[$viewer_fragment])) {
$this->workspace[$viewer_fragment] = array();
}
$this->workspace[$viewer_fragment] += $objects;
return $this;
}
/**
* Retrieve objects from the query workspace. For more discussion about the
* workspace mechanism, see @{method:putObjectsInWorkspace}. This method
* searches both the current query's workspace and the workspaces of parent
* queries.
*
* @param list<phid> List of PHIDs to retrieve.
* @return this
* @task workspace
*/
public function getObjectsFromWorkspace(array $phids) {
$parent = $this->getParentQuery();
if ($parent) {
return $parent->getObjectsFromWorkspace($phids);
}
$viewer_fragment = $this->getViewer()->getCacheFragment();
$results = array();
foreach ($phids as $key => $phid) {
if (isset($this->workspace[$viewer_fragment][$phid])) {
$results[$phid] = $this->workspace[$viewer_fragment][$phid];
unset($phids[$key]);
}
}
return $results;
}
/**
* Mark PHIDs as in flight.
*
* PHIDs which are "in flight" are actively being queried for. Using this
* list can prevent infinite query loops by aborting queries which cycle.
*
* @param list<phid> List of PHIDs which are now in flight.
* @return this
*/
public function putPHIDsInFlight(array $phids) {
foreach ($phids as $phid) {
$this->inFlightPHIDs[$phid] = $phid;
}
return $this;
}
/**
* Get PHIDs which are currently in flight.
*
* PHIDs which are "in flight" are actively being queried for.
*
* @return map<phid, phid> PHIDs currently in flight.
*/
public function getPHIDsInFlight() {
$results = $this->inFlightPHIDs;
if ($this->getParentQuery()) {
$results += $this->getParentQuery()->getPHIDsInFlight();
}
return $results;
}
/* -( Policy Query Implementation )---------------------------------------- */
/**
* Get the number of results @{method:loadPage} should load. If the value is
* 0, @{method:loadPage} should load all available results.
*
* @return int The number of results to load, or 0 for all results.
* @task policyimpl
*/
final protected function getRawResultLimit() {
return $this->rawResultLimit;
}
/**
* Hook invoked before query execution. Generally, implementations should
* reset any internal cursors.
*
* @return void
* @task policyimpl
*/
protected function willExecute() {
return;
}
/**
* Load a raw page of results. Generally, implementations should load objects
* from the database. They should attempt to return the number of results
* hinted by @{method:getRawResultLimit}.
*
* @return list<PhabricatorPolicyInterface> List of filterable policy objects.
* @task policyimpl
*/
abstract protected function loadPage();
/**
* Update internal state so that the next call to @{method:loadPage} will
* return new results. Generally, you should adjust a cursor position based
* on the provided result page.
*
* @param list<PhabricatorPolicyInterface> The current page of results.
* @return void
* @task policyimpl
*/
abstract protected function nextPage(array $page);
/**
* Hook for applying a page filter prior to the privacy filter. This allows
* you to drop some items from the result set without creating problems with
* pagination or cursor updates. You can also load and attach data which is
* required to perform policy filtering.
*
* Generally, you should load non-policy data and perform non-policy filtering
* later, in @{method:didFilterPage}. Strictly fewer objects will make it that
* far (so the program will load less data) and subqueries from that context
* can use the query workspace to further reduce query load.
*
* This method will only be called if data is available. Implementations
* do not need to handle the case of no results specially.
*
* @param list<wild> Results from `loadPage()`.
* @return list<PhabricatorPolicyInterface> Objects for policy filtering.
* @task policyimpl
*/
protected function willFilterPage(array $page) {
return $page;
}
/**
* Hook for performing additional non-policy loading or filtering after an
* object has satisfied all policy checks. Generally, this means loading and
* attaching related data.
*
* Subqueries executed during this phase can use the query workspace, which
* may improve performance or make circular policies resolvable. Data which
* is not necessary for policy filtering should generally be loaded here.
*
* This callback can still filter objects (for example, if attachable data
* is discovered to not exist), but should not do so for policy reasons.
*
* This method will only be called if data is available. Implementations do
* not need to handle the case of no results specially.
*
* @param list<wild> Results from @{method:willFilterPage()}.
* @return list<PhabricatorPolicyInterface> Objects after additional
* non-policy processing.
*/
protected function didFilterPage(array $page) {
return $page;
}
/**
* Hook for removing filtered results from alternate result sets. This
* hook will be called with any objects which were returned by the query but
* filtered for policy reasons. The query should remove them from any cached
* or partial result sets.
*
* @param list<wild> List of objects that should not be returned by alternate
* result mechanisms.
* @return void
* @task policyimpl
*/
protected function didFilterResults(array $results) {
return;
}
/**
* Hook for applying final adjustments before results are returned. This is
* used by @{class:PhabricatorCursorPagedPolicyAwareQuery} to reverse results
* that are queried during reverse paging.
*
* @param list<PhabricatorPolicyInterface> Query results.
* @return list<PhabricatorPolicyInterface> Final results.
* @task policyimpl
*/
protected function didLoadResults(array $results) {
return $results;
}
/**
* Allows a subclass to disable policy filtering. This method is dangerous.
* It should be used only if the query loads data which has already been
* filtered (for example, because it wraps some other query which uses
* normal policy filtering).
*
* @return bool True to disable all policy filtering.
* @task policyimpl
*/
protected function shouldDisablePolicyFiltering() {
return false;
}
/**
* If this query belongs to an application, return the application class name
* here. This will prevent the query from returning results if the viewer can
* not access the application.
*
* If this query does not belong to an application, return `null`.
*
* @return string|null Application class name.
*/
abstract public function getQueryApplicationClass();
/**
* Determine if the viewer has permission to use this query's application.
* For queries which aren't part of an application, this method always returns
* true.
*
* @return bool True if the viewer has application-level permission to
* execute the query.
*/
public function canViewerUseQueryApplication() {
$class = $this->getQueryApplicationClass();
if (!$class) {
return true;
}
$viewer = $this->getViewer();
return PhabricatorApplication::isClassInstalledForViewer($class, $viewer);
}
private function applyWillFilterPageExtensions(array $page) {
$bridges = array();
foreach ($page as $key => $object) {
if ($object instanceof DoorkeeperBridgedObjectInterface) {
$bridges[$key] = $object;
}
}
if ($bridges) {
$external_phids = array();
foreach ($bridges as $bridge) {
$external_phid = $bridge->getBridgedObjectPHID();
if ($external_phid) {
$external_phids[$key] = $external_phid;
}
}
if ($external_phids) {
$external_objects = id(new DoorkeeperExternalObjectQuery())
->setViewer($this->getViewer())
->withPHIDs($external_phids)
->execute();
$external_objects = mpull($external_objects, null, 'getPHID');
} else {
$external_objects = array();
}
foreach ($bridges as $key => $bridge) {
$external_phid = idx($external_phids, $key);
if (!$external_phid) {
$bridge->attachBridgedObject(null);
continue;
}
$external_object = idx($external_objects, $external_phid);
if (!$external_object) {
$this->didRejectResult($bridge);
unset($page[$key]);
continue;
}
$bridge->attachBridgedObject($external_object);
}
}
return $page;
}
}
diff --git a/src/infrastructure/query/policy/PhabricatorQueryCursor.php b/src/infrastructure/query/policy/PhabricatorQueryCursor.php
new file mode 100644
index 000000000..4ec126313
--- /dev/null
+++ b/src/infrastructure/query/policy/PhabricatorQueryCursor.php
@@ -0,0 +1,47 @@
+<?php
+
+final class PhabricatorQueryCursor
+ extends Phobject {
+
+ private $object;
+ private $rawRow;
+
+ public function setObject($object) {
+ $this->object = $object;
+ return $this;
+ }
+
+ public function getObject() {
+ return $this->object;
+ }
+
+ public function setRawRow(array $raw_row) {
+ $this->rawRow = $raw_row;
+ return $this;
+ }
+
+ public function getRawRow() {
+ return $this->rawRow;
+ }
+
+ public function getRawRowProperty($key) {
+ if (!is_array($this->rawRow)) {
+ throw new Exception(
+ pht(
+ 'Caller is trying to "getRawRowProperty()" with key "%s", but this '.
+ 'cursor has no raw row.',
+ $key));
+ }
+
+ if (!array_key_exists($key, $this->rawRow)) {
+ throw new Exception(
+ pht(
+ 'Caller is trying to access raw row property "%s", but the row '.
+ 'does not have this property.',
+ $key));
+ }
+
+ return $this->rawRow[$key];
+ }
+
+}
diff --git a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php
index e47d701df..99603da56 100644
--- a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php
+++ b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php
@@ -1,328 +1,340 @@
<?php
/**
* @task config Configuring Storage
*/
abstract class PhabricatorLiskDAO extends LiskDAO {
private static $namespaceStack = array();
+ private $forcedNamespace;
const ATTACHABLE = '<attachable>';
const CONFIG_APPLICATION_SERIALIZERS = 'phabricator/serializers';
/* -( Configuring Storage )------------------------------------------------ */
/**
* @task config
*/
public static function pushStorageNamespace($namespace) {
self::$namespaceStack[] = $namespace;
}
/**
* @task config
*/
public static function popStorageNamespace() {
array_pop(self::$namespaceStack);
}
/**
* @task config
*/
public static function getDefaultStorageNamespace() {
return PhabricatorEnv::getEnvConfig('storage.default-namespace');
}
/**
* @task config
*/
public static function getStorageNamespace() {
$namespace = end(self::$namespaceStack);
if (!strlen($namespace)) {
$namespace = self::getDefaultStorageNamespace();
}
if (!strlen($namespace)) {
throw new Exception(pht('No storage namespace configured!'));
}
return $namespace;
}
+ public function setForcedStorageNamespace($namespace) {
+ $this->forcedNamespace = $namespace;
+ return $this;
+ }
+
/**
* @task config
*/
protected function establishLiveConnection($mode) {
$namespace = self::getStorageNamespace();
$database = $namespace.'_'.$this->getApplicationName();
$is_readonly = PhabricatorEnv::isReadOnly();
if ($is_readonly && ($mode != 'r')) {
$this->raiseImproperWrite($database);
}
$connection = $this->newClusterConnection(
$this->getApplicationName(),
$database,
$mode);
// TODO: This should be testing if the mode is "r", but that would probably
// break a lot of things. Perform a more narrow test for readonly mode
// until we have greater certainty that this works correctly most of the
// time.
if ($is_readonly) {
$connection->setReadOnly(true);
}
return $connection;
}
private function newClusterConnection($application, $database, $mode) {
$master = PhabricatorDatabaseRef::getMasterDatabaseRefForApplication(
$application);
$master_exception = null;
if ($master && !$master->isSevered()) {
$connection = $master->newApplicationConnection($database);
if ($master->isReachable($connection)) {
return $connection;
} else {
if ($mode == 'w') {
$this->raiseImpossibleWrite($database);
}
PhabricatorEnv::setReadOnly(
true,
PhabricatorEnv::READONLY_UNREACHABLE);
$master_exception = $master->getConnectionException();
}
}
$replica = PhabricatorDatabaseRef::getReplicaDatabaseRefForApplication(
$application);
if ($replica) {
$connection = $replica->newApplicationConnection($database);
$connection->setReadOnly(true);
if ($replica->isReachable($connection)) {
return $connection;
}
}
if (!$master && !$replica) {
$this->raiseUnconfigured($database);
}
$this->raiseUnreachable($database, $master_exception);
}
private function raiseImproperWrite($database) {
throw new PhabricatorClusterImproperWriteException(
pht(
'Unable to establish a write-mode connection (to application '.
'database "%s") because Phabricator is in read-only mode. Whatever '.
'you are trying to do does not function correctly in read-only mode.',
$database));
}
private function raiseImpossibleWrite($database) {
throw new PhabricatorClusterImpossibleWriteException(
pht(
'Unable to connect to master database ("%s"). This is a severe '.
'failure; your request did not complete.',
$database));
}
private function raiseUnconfigured($database) {
throw new Exception(
pht(
'Unable to establish a connection to any database host '.
'(while trying "%s"). No masters or replicas are configured.',
$database));
}
private function raiseUnreachable($database, Exception $proxy = null) {
$message = pht(
'Unable to establish a connection to any database host '.
'(while trying "%s"). All masters and replicas are completely '.
'unreachable.',
$database);
if ($proxy) {
$proxy_message = pht(
'%s: %s',
get_class($proxy),
$proxy->getMessage());
$message = $message."\n\n".$proxy_message;
}
throw new PhabricatorClusterStrandedException($message);
}
/**
* @task config
*/
public function getTableName() {
$str = 'phabricator';
$len = strlen($str);
$class = strtolower(get_class($this));
if (!strncmp($class, $str, $len)) {
$class = substr($class, $len);
}
$app = $this->getApplicationName();
if (!strncmp($class, $app, strlen($app))) {
$class = substr($class, strlen($app));
}
if (strlen($class)) {
return $app.'_'.$class;
} else {
return $app;
}
}
/**
* @task config
*/
abstract public function getApplicationName();
protected function getDatabaseName() {
- return self::getStorageNamespace().'_'.$this->getApplicationName();
+ if ($this->forcedNamespace) {
+ $namespace = $this->forcedNamespace;
+ } else {
+ $namespace = self::getStorageNamespace();
+ }
+
+ return $namespace.'_'.$this->getApplicationName();
}
/**
* Break a list of escaped SQL statement fragments (e.g., VALUES lists for
* INSERT, previously built with @{function:qsprintf}) into chunks which will
* fit under the MySQL 'max_allowed_packet' limit.
*
* If a statement is too large to fit within the limit, it is broken into
* its own chunk (but might fail when the query executes).
*/
public static function chunkSQL(
array $fragments,
$limit = null) {
if ($limit === null) {
// NOTE: Hard-code this at 1MB for now, minus a 10% safety buffer.
// Eventually we could query MySQL or let the user configure it.
$limit = (int)((1024 * 1024) * 0.90);
}
$result = array();
$chunk = array();
$len = 0;
$glue_len = strlen(', ');
foreach ($fragments as $fragment) {
if ($fragment instanceof PhutilQueryString) {
$this_len = strlen($fragment->getUnmaskedString());
} else {
$this_len = strlen($fragment);
}
if ($chunk) {
// Chunks after the first also imply glue.
$this_len += $glue_len;
}
if ($len + $this_len <= $limit) {
$len += $this_len;
$chunk[] = $fragment;
} else {
if ($chunk) {
$result[] = $chunk;
}
$len = ($this_len - $glue_len);
$chunk = array($fragment);
}
}
if ($chunk) {
$result[] = $chunk;
}
return $result;
}
protected function assertAttached($property) {
if ($property === self::ATTACHABLE) {
throw new PhabricatorDataNotAttachedException($this);
}
return $property;
}
protected function assertAttachedKey($value, $key) {
$this->assertAttached($value);
if (!array_key_exists($key, $value)) {
throw new PhabricatorDataNotAttachedException($this);
}
return $value[$key];
}
protected function detectEncodingForStorage($string) {
return phutil_is_utf8($string) ? 'utf8' : null;
}
protected function getUTF8StringFromStorage($string, $encoding) {
if ($encoding == 'utf8') {
return $string;
}
if (function_exists('mb_detect_encoding')) {
if (strlen($encoding)) {
$try_encodings = array(
$encoding,
);
} else {
// TODO: This is pretty much a guess, and probably needs to be
// configurable in the long run.
$try_encodings = array(
'JIS',
'EUC-JP',
'SJIS',
'ISO-8859-1',
);
}
$guess = mb_detect_encoding($string, $try_encodings);
if ($guess) {
return mb_convert_encoding($string, 'UTF-8', $guess);
}
}
return phutil_utf8ize($string);
}
protected function willReadData(array &$data) {
parent::willReadData($data);
static $custom;
if ($custom === null) {
$custom = $this->getConfigOption(self::CONFIG_APPLICATION_SERIALIZERS);
}
if ($custom) {
foreach ($custom as $key => $serializer) {
$data[$key] = $serializer->willReadValue($data[$key]);
}
}
}
protected function willWriteData(array &$data) {
static $custom;
if ($custom === null) {
$custom = $this->getConfigOption(self::CONFIG_APPLICATION_SERIALIZERS);
}
if ($custom) {
foreach ($custom as $key => $serializer) {
$data[$key] = $serializer->willWriteValue($data[$key]);
}
}
parent::willWriteData($data);
}
}
diff --git a/src/infrastructure/storage/lisk/PhabricatorQueryIterator.php b/src/infrastructure/storage/lisk/PhabricatorQueryIterator.php
index 03aaf5707..648b83863 100644
--- a/src/infrastructure/storage/lisk/PhabricatorQueryIterator.php
+++ b/src/infrastructure/storage/lisk/PhabricatorQueryIterator.php
@@ -1,41 +1,43 @@
<?php
final class PhabricatorQueryIterator extends PhutilBufferedIterator {
private $query;
private $pager;
public function __construct(PhabricatorCursorPagedPolicyAwareQuery $query) {
$this->query = $query;
}
protected function didRewind() {
$this->pager = new AphrontCursorPagerView();
}
public function key() {
return $this->current()->getID();
}
protected function loadPage() {
if (!$this->pager) {
return array();
}
$pager = clone $this->pager;
$query = clone $this->query;
+ $query->setDisableOverheating(true);
+
$results = $query->executeWithCursorPager($pager);
// If we got less than a full page of results, this was the last set of
// results. Throw away the pager so we end iteration.
if (!$pager->getHasMoreResults()) {
$this->pager = null;
} else {
$this->pager->setAfterID($pager->getNextPageID());
}
return $results;
}
}
diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php
index 5bc83972d..acbbb4fbd 100644
--- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php
+++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php
@@ -1,1283 +1,1283 @@
<?php
abstract class PhabricatorStorageManagementWorkflow
extends PhabricatorManagementWorkflow {
private $apis = array();
private $dryRun;
private $force;
private $patches;
private $didInitialize;
final public function setAPIs(array $apis) {
$this->apis = $apis;
return $this;
}
final public function getAnyAPI() {
return head($this->getAPIs());
}
final public function getMasterAPIs() {
$apis = $this->getAPIs();
$results = array();
foreach ($apis as $api) {
if ($api->getRef()->getIsMaster()) {
$results[] = $api;
}
}
if (!$results) {
throw new PhutilArgumentUsageException(
pht(
'This command only operates on database masters, but the selected '.
'database hosts do not include any masters.'));
}
return $results;
}
final public function getSingleAPI() {
$apis = $this->getAPIs();
if (count($apis) == 1) {
return head($apis);
}
throw new PhutilArgumentUsageException(
pht(
'Phabricator is configured in cluster mode, with multiple database '.
'hosts. Use "--host" to specify which host you want to operate on.'));
}
final public function getAPIs() {
return $this->apis;
}
final protected function isDryRun() {
return $this->dryRun;
}
final protected function setDryRun($dry_run) {
$this->dryRun = $dry_run;
return $this;
}
final protected function isForce() {
return $this->force;
}
final protected function setForce($force) {
$this->force = $force;
return $this;
}
public function getPatches() {
return $this->patches;
}
public function setPatches(array $patches) {
assert_instances_of($patches, 'PhabricatorStoragePatch');
$this->patches = $patches;
return $this;
}
protected function isReadOnlyWorkflow() {
return false;
}
public function execute(PhutilArgumentParser $args) {
$this->setDryRun($args->getArg('dryrun'));
$this->setForce($args->getArg('force'));
if (!$this->isReadOnlyWorkflow()) {
if (PhabricatorEnv::isReadOnly()) {
if ($this->isForce()) {
PhabricatorEnv::setReadOnly(false, null);
} else {
throw new PhutilArgumentUsageException(
pht(
'Phabricator is currently in read-only mode. Use --force to '.
'override this mode.'));
}
}
}
return $this->didExecute($args);
}
public function didExecute(PhutilArgumentParser $args) {}
private function loadSchemata(PhabricatorStorageManagementAPI $api) {
$query = id(new PhabricatorConfigSchemaQuery());
$ref = $api->getRef();
$ref_key = $ref->getRefKey();
$query->setAPIs(array($api));
$query->setRefs(array($ref));
$actual = $query->loadActualSchemata();
$expect = $query->loadExpectedSchemata();
$comp = $query->buildComparisonSchemata($expect, $actual);
return array(
$comp[$ref_key],
$expect[$ref_key],
$actual[$ref_key],
);
}
final protected function adjustSchemata(
PhabricatorStorageManagementAPI $api,
$unsafe) {
$lock = $this->lock($api);
try {
$err = $this->doAdjustSchemata($api, $unsafe);
// Analyze tables if we're not doing a dry run and adjustments are either
// all clear or have minor errors like surplus tables.
if (!$this->dryRun) {
$should_analyze = (($err == 0) || ($err == 2));
if ($should_analyze) {
$this->analyzeTables($api);
}
}
} catch (Exception $ex) {
$lock->unlock();
throw $ex;
}
$lock->unlock();
return $err;
}
final private function doAdjustSchemata(
PhabricatorStorageManagementAPI $api,
$unsafe) {
$console = PhutilConsole::getConsole();
$console->writeOut(
"%s\n",
pht(
'Verifying database schemata on "%s"...',
$api->getRef()->getRefKey()));
list($adjustments, $errors) = $this->findAdjustments($api);
if (!$adjustments) {
$console->writeOut(
"%s\n",
pht('Found no adjustments for schemata.'));
return $this->printErrors($errors, 0);
}
if (!$this->force && !$api->isCharacterSetAvailable('utf8mb4')) {
$message = pht(
"You have an old version of MySQL (older than 5.5) which does not ".
"support the utf8mb4 character set. We strongly recommend upgrading ".
"to 5.5 or newer.\n\n".
"If you apply adjustments now and later update MySQL to 5.5 or newer, ".
"you'll need to apply adjustments again (and they will take a long ".
"time).\n\n".
"You can exit this workflow, update MySQL now, and then run this ".
"workflow again. This is recommended, but may cause a lot of downtime ".
"right now.\n\n".
"You can exit this workflow, continue using Phabricator without ".
"applying adjustments, update MySQL at a later date, and then run ".
"this workflow again. This is also a good approach, and will let you ".
"delay downtime until later.\n\n".
"You can proceed with this workflow, and then optionally update ".
"MySQL at a later date. After you do, you'll need to apply ".
"adjustments again.\n\n".
"For more information, see \"Managing Storage Adjustments\" in ".
"the documentation.");
$console->writeOut(
"\n**<bg:yellow> %s </bg>**\n\n%s\n",
pht('OLD MySQL VERSION'),
phutil_console_wrap($message));
$prompt = pht('Continue with old MySQL version?');
if (!phutil_console_confirm($prompt, $default_no = true)) {
return;
}
}
$table = id(new PhutilConsoleTable())
->addColumn('database', array('title' => pht('Database')))
->addColumn('table', array('title' => pht('Table')))
->addColumn('name', array('title' => pht('Name')))
->addColumn('info', array('title' => pht('Issues')));
foreach ($adjustments as $adjust) {
$info = array();
foreach ($adjust['issues'] as $issue) {
$info[] = PhabricatorConfigStorageSchema::getIssueName($issue);
}
$table->addRow(array(
'database' => $adjust['database'],
'table' => idx($adjust, 'table'),
'name' => idx($adjust, 'name'),
'info' => implode(', ', $info),
));
}
$console->writeOut("\n\n");
$table->draw();
if ($this->dryRun) {
$console->writeOut(
"%s\n",
pht('DRYRUN: Would apply adjustments.'));
return 0;
} else if ($this->didInitialize) {
// If we just initialized the database, continue without prompting. This
// is nicer for first-time setup and there's no reasonable reason any
// user would ever answer "no" to the prompt against an empty schema.
} else if (!$this->force) {
$console->writeOut(
"\n%s\n",
pht(
"Found %s adjustment(s) to apply, detailed above.\n\n".
"You can review adjustments in more detail from the web interface, ".
"in Config > Database Status. To better understand the adjustment ".
"workflow, see \"Managing Storage Adjustments\" in the ".
"documentation.\n\n".
"MySQL needs to copy table data to make some adjustments, so these ".
"migrations may take some time.",
phutil_count($adjustments)));
$prompt = pht('Apply these schema adjustments?');
if (!phutil_console_confirm($prompt, $default_no = true)) {
return 1;
}
}
$console->writeOut(
"%s\n",
pht('Applying schema adjustments...'));
$conn = $api->getConn(null);
if ($unsafe) {
queryfx($conn, 'SET SESSION sql_mode = %s', '');
} else {
queryfx($conn, 'SET SESSION sql_mode = %s', 'STRICT_ALL_TABLES');
}
$failed = array();
// We make changes in several phases.
$phases = array(
// Drop surplus autoincrements. This allows us to drop primary keys on
// autoincrement columns.
'drop_auto',
// Drop all keys we're going to adjust. This prevents them from
// interfering with column changes.
'drop_keys',
// Apply all database, table, and column changes.
'main',
// Restore adjusted keys.
'add_keys',
// Add missing autoincrements.
'add_auto',
);
$bar = id(new PhutilConsoleProgressBar())
->setTotal(count($adjustments) * count($phases));
foreach ($phases as $phase) {
foreach ($adjustments as $adjust) {
try {
switch ($adjust['kind']) {
case 'database':
if ($phase == 'main') {
queryfx(
$conn,
'ALTER DATABASE %T CHARACTER SET = %s COLLATE = %s',
$adjust['database'],
$adjust['charset'],
$adjust['collation']);
}
break;
case 'table':
if ($phase == 'main') {
queryfx(
$conn,
'ALTER TABLE %T.%T COLLATE = %s, ENGINE = %s',
$adjust['database'],
$adjust['table'],
$adjust['collation'],
$adjust['engine']);
}
break;
case 'column':
$apply = false;
$auto = false;
$new_auto = idx($adjust, 'auto');
if ($phase == 'drop_auto') {
if ($new_auto === false) {
$apply = true;
$auto = false;
}
} else if ($phase == 'main') {
$apply = true;
if ($new_auto === false) {
$auto = false;
} else {
$auto = $adjust['is_auto'];
}
} else if ($phase == 'add_auto') {
if ($new_auto === true) {
$apply = true;
$auto = true;
}
}
if ($apply) {
$parts = array();
if ($auto) {
$parts[] = qsprintf(
$conn,
'AUTO_INCREMENT');
}
if ($adjust['charset']) {
switch ($adjust['charset']) {
case 'binary':
$charset_value = qsprintf($conn, 'binary');
break;
case 'utf8':
$charset_value = qsprintf($conn, 'utf8');
break;
case 'utf8mb4':
$charset_value = qsprintf($conn, 'utf8mb4');
break;
default:
throw new Exception(
pht(
'Unsupported character set "%s".',
$adjust['charset']));
}
switch ($adjust['collation']) {
case 'binary':
$collation_value = qsprintf($conn, 'binary');
break;
case 'utf8_general_ci':
$collation_value = qsprintf($conn, 'utf8_general_ci');
break;
case 'utf8mb4_bin':
$collation_value = qsprintf($conn, 'utf8mb4_bin');
break;
case 'utf8mb4_unicode_ci':
$collation_value = qsprintf($conn, 'utf8mb4_unicode_ci');
break;
default:
throw new Exception(
pht(
'Unsupported collation set "%s".',
$adjust['collation']));
}
$parts[] = qsprintf(
$conn,
'CHARACTER SET %Q COLLATE %Q',
$charset_value,
$collation_value);
}
if ($parts) {
$parts = qsprintf($conn, '%LJ', $parts);
} else {
$parts = qsprintf($conn, '');
}
if ($adjust['nullable']) {
$nullable = qsprintf($conn, 'NULL');
} else {
$nullable = qsprintf($conn, 'NOT NULL');
}
// TODO: We're using "%Z" here for the column type, which is
// technically unsafe. It would be nice to be able to use "%Q"
// instead, but this requires a fair amount of legwork to
// enumerate all column types.
queryfx(
$conn,
'ALTER TABLE %T.%T MODIFY %T %Z %Q %Q',
$adjust['database'],
$adjust['table'],
$adjust['name'],
$adjust['type'],
$parts,
$nullable);
}
break;
case 'key':
if (($phase == 'drop_keys') && $adjust['exists']) {
if ($adjust['name'] == 'PRIMARY') {
- $key_name = 'PRIMARY KEY';
+ $key_name = qsprintf($conn, 'PRIMARY KEY');
} else {
$key_name = qsprintf($conn, 'KEY %T', $adjust['name']);
}
queryfx(
$conn,
'ALTER TABLE %T.%T DROP %Q',
$adjust['database'],
$adjust['table'],
$key_name);
}
if (($phase == 'add_keys') && $adjust['keep']) {
// Different keys need different creation syntax. Notable
// special cases are primary keys and fulltext keys.
if ($adjust['name'] == 'PRIMARY') {
$key_name = qsprintf($conn, 'PRIMARY KEY');
} else if ($adjust['indexType'] == 'FULLTEXT') {
$key_name = qsprintf($conn, 'FULLTEXT %T', $adjust['name']);
} else {
if ($adjust['unique']) {
$key_name = qsprintf(
$conn,
'UNIQUE KEY %T',
$adjust['name']);
} else {
$key_name = qsprintf(
$conn,
'/* NONUNIQUE */ KEY %T',
$adjust['name']);
}
}
queryfx(
$conn,
'ALTER TABLE %T.%T ADD %Q (%LK)',
$adjust['database'],
$adjust['table'],
$key_name,
$adjust['columns']);
}
break;
default:
throw new Exception(
pht('Unknown schema adjustment kind "%s"!', $adjust['kind']));
}
} catch (AphrontQueryException $ex) {
$failed[] = array($adjust, $ex);
}
$bar->update(1);
}
}
$bar->done();
if (!$failed) {
$console->writeOut(
"%s\n",
pht('Completed applying all schema adjustments.'));
$err = 0;
} else {
$table = id(new PhutilConsoleTable())
->addColumn('target', array('title' => pht('Target')))
->addColumn('error', array('title' => pht('Error')));
foreach ($failed as $failure) {
list($adjust, $ex) = $failure;
$pieces = array_select_keys(
$adjust,
array('database', 'table', 'name'));
$pieces = array_filter($pieces);
$target = implode('.', $pieces);
$table->addRow(
array(
'target' => $target,
'error' => $ex->getMessage(),
));
}
$console->writeOut("\n");
$table->draw();
$console->writeOut(
"\n%s\n",
pht('Failed to make some schema adjustments, detailed above.'));
$console->writeOut(
"%s\n",
pht(
'For help troubleshooting adjustments, see "Managing Storage '.
'Adjustments" in the documentation.'));
$err = 1;
}
return $this->printErrors($errors, $err);
}
private function findAdjustments(
PhabricatorStorageManagementAPI $api) {
list($comp, $expect, $actual) = $this->loadSchemata($api);
$issue_charset = PhabricatorConfigStorageSchema::ISSUE_CHARSET;
$issue_collation = PhabricatorConfigStorageSchema::ISSUE_COLLATION;
$issue_columntype = PhabricatorConfigStorageSchema::ISSUE_COLUMNTYPE;
$issue_surpluskey = PhabricatorConfigStorageSchema::ISSUE_SURPLUSKEY;
$issue_missingkey = PhabricatorConfigStorageSchema::ISSUE_MISSINGKEY;
$issue_columns = PhabricatorConfigStorageSchema::ISSUE_KEYCOLUMNS;
$issue_unique = PhabricatorConfigStorageSchema::ISSUE_UNIQUE;
$issue_longkey = PhabricatorConfigStorageSchema::ISSUE_LONGKEY;
$issue_auto = PhabricatorConfigStorageSchema::ISSUE_AUTOINCREMENT;
$issue_engine = PhabricatorConfigStorageSchema::ISSUE_ENGINE;
$adjustments = array();
$errors = array();
foreach ($comp->getDatabases() as $database_name => $database) {
foreach ($this->findErrors($database) as $issue) {
$errors[] = array(
'database' => $database_name,
'issue' => $issue,
);
}
$expect_database = $expect->getDatabase($database_name);
$actual_database = $actual->getDatabase($database_name);
if (!$expect_database || !$actual_database) {
// If there's a real issue here, skip this stuff.
continue;
}
if ($actual_database->getAccessDenied()) {
// If we can't access the database, we can't access the tables either.
continue;
}
$issues = array();
if ($database->hasIssue($issue_charset)) {
$issues[] = $issue_charset;
}
if ($database->hasIssue($issue_collation)) {
$issues[] = $issue_collation;
}
if ($issues) {
$adjustments[] = array(
'kind' => 'database',
'database' => $database_name,
'issues' => $issues,
'charset' => $expect_database->getCharacterSet(),
'collation' => $expect_database->getCollation(),
);
}
foreach ($database->getTables() as $table_name => $table) {
foreach ($this->findErrors($table) as $issue) {
$errors[] = array(
'database' => $database_name,
'table' => $table_name,
'issue' => $issue,
);
}
$expect_table = $expect_database->getTable($table_name);
$actual_table = $actual_database->getTable($table_name);
if (!$expect_table || !$actual_table) {
continue;
}
$issues = array();
if ($table->hasIssue($issue_collation)) {
$issues[] = $issue_collation;
}
if ($table->hasIssue($issue_engine)) {
$issues[] = $issue_engine;
}
if ($issues) {
$adjustments[] = array(
'kind' => 'table',
'database' => $database_name,
'table' => $table_name,
'issues' => $issues,
'collation' => $expect_table->getCollation(),
'engine' => $expect_table->getEngine(),
);
}
foreach ($table->getColumns() as $column_name => $column) {
foreach ($this->findErrors($column) as $issue) {
$errors[] = array(
'database' => $database_name,
'table' => $table_name,
'name' => $column_name,
'issue' => $issue,
);
}
$expect_column = $expect_table->getColumn($column_name);
$actual_column = $actual_table->getColumn($column_name);
if (!$expect_column || !$actual_column) {
continue;
}
$issues = array();
if ($column->hasIssue($issue_collation)) {
$issues[] = $issue_collation;
}
if ($column->hasIssue($issue_charset)) {
$issues[] = $issue_charset;
}
if ($column->hasIssue($issue_columntype)) {
$issues[] = $issue_columntype;
}
if ($column->hasIssue($issue_auto)) {
$issues[] = $issue_auto;
}
if ($issues) {
if ($expect_column->getCharacterSet() === null) {
// For non-text columns, we won't be specifying a collation or
// character set.
$charset = null;
$collation = null;
} else {
$charset = $expect_column->getCharacterSet();
$collation = $expect_column->getCollation();
}
$adjustment = array(
'kind' => 'column',
'database' => $database_name,
'table' => $table_name,
'name' => $column_name,
'issues' => $issues,
'collation' => $collation,
'charset' => $charset,
'type' => $expect_column->getColumnType(),
// NOTE: We don't adjust column nullability because it is
// dangerous, so always use the current nullability.
'nullable' => $actual_column->getNullable(),
// NOTE: This always stores the current value, because we have
// to make these updates separately.
'is_auto' => $actual_column->getAutoIncrement(),
);
if ($column->hasIssue($issue_auto)) {
$adjustment['auto'] = $expect_column->getAutoIncrement();
}
$adjustments[] = $adjustment;
}
}
foreach ($table->getKeys() as $key_name => $key) {
foreach ($this->findErrors($key) as $issue) {
$errors[] = array(
'database' => $database_name,
'table' => $table_name,
'name' => $key_name,
'issue' => $issue,
);
}
$expect_key = $expect_table->getKey($key_name);
$actual_key = $actual_table->getKey($key_name);
$issues = array();
$keep_key = true;
if ($key->hasIssue($issue_surpluskey)) {
$issues[] = $issue_surpluskey;
$keep_key = false;
}
if ($key->hasIssue($issue_missingkey)) {
$issues[] = $issue_missingkey;
}
if ($key->hasIssue($issue_columns)) {
$issues[] = $issue_columns;
}
if ($key->hasIssue($issue_unique)) {
$issues[] = $issue_unique;
}
// NOTE: We can't really fix this, per se, but we may need to remove
// the key to change the column type. In the best case, the new
// column type won't be overlong and recreating the key really will
// fix the issue. In the worst case, we get the right column type and
// lose the key, which is still better than retaining the key having
// the wrong column type.
if ($key->hasIssue($issue_longkey)) {
$issues[] = $issue_longkey;
}
if ($issues) {
$adjustment = array(
'kind' => 'key',
'database' => $database_name,
'table' => $table_name,
'name' => $key_name,
'issues' => $issues,
'exists' => (bool)$actual_key,
'keep' => $keep_key,
);
if ($keep_key) {
$adjustment += array(
'columns' => $expect_key->getColumnNames(),
'unique' => $expect_key->getUnique(),
'indexType' => $expect_key->getIndexType(),
);
}
$adjustments[] = $adjustment;
}
}
}
}
return array($adjustments, $errors);
}
private function findErrors(PhabricatorConfigStorageSchema $schema) {
$result = array();
foreach ($schema->getLocalIssues() as $issue) {
$status = PhabricatorConfigStorageSchema::getIssueStatus($issue);
if ($status == PhabricatorConfigStorageSchema::STATUS_FAIL) {
$result[] = $issue;
}
}
return $result;
}
private function printErrors(array $errors, $default_return) {
if (!$errors) {
return $default_return;
}
$console = PhutilConsole::getConsole();
$table = id(new PhutilConsoleTable())
->addColumn('target', array('title' => pht('Target')))
->addColumn('error', array('title' => pht('Error')));
$any_surplus = false;
$all_surplus = true;
$any_access = false;
$all_access = true;
foreach ($errors as $error) {
$pieces = array_select_keys(
$error,
array('database', 'table', 'name'));
$pieces = array_filter($pieces);
$target = implode('.', $pieces);
$name = PhabricatorConfigStorageSchema::getIssueName($error['issue']);
$issue = $error['issue'];
if ($issue === PhabricatorConfigStorageSchema::ISSUE_SURPLUS) {
$any_surplus = true;
} else {
$all_surplus = false;
}
if ($issue === PhabricatorConfigStorageSchema::ISSUE_ACCESSDENIED) {
$any_access = true;
} else {
$all_access = false;
}
$table->addRow(
array(
'target' => $target,
'error' => $name,
));
}
$console->writeOut("\n");
$table->draw();
$console->writeOut("\n");
$message = array();
if ($all_surplus) {
$message[] = pht(
'You have surplus schemata (extra tables or columns which Phabricator '.
'does not expect). For information on resolving these '.
'issues, see the "Surplus Schemata" section in the "Managing Storage '.
'Adjustments" article in the documentation.');
} else if ($all_access) {
$message[] = pht(
'The user you are connecting to MySQL with does not have the correct '.
'permissions, and can not access some databases or tables that it '.
'needs to be able to access. GRANT the user additional permissions.');
} else {
$message[] = pht(
'The schemata have errors (detailed above) which the adjustment '.
'workflow can not fix.');
if ($any_access) {
$message[] = pht(
'Some of these errors are caused by access control problems. '.
'The user you are connecting with does not have permission to see '.
'all of the database or tables that Phabricator uses. You need to '.
'GRANT the user more permission, or use a different user.');
}
if ($any_surplus) {
$message[] = pht(
'Some of these errors are caused by surplus schemata (extra '.
'tables or columns which Phabricator does not expect). These are '.
'not serious. For information on resolving these issues, see the '.
'"Surplus Schemata" section in the "Managing Storage Adjustments" '.
'article in the documentation.');
}
$message[] = pht(
'If you are not developing Phabricator itself, report this issue to '.
'the upstream.');
$message[] = pht(
'If you are developing Phabricator, these errors usually indicate '.
'that your schema specifications do not agree with the schemata your '.
'code actually builds.');
}
$message = implode("\n\n", $message);
if ($all_surplus) {
$console->writeOut(
"**<bg:yellow> %s </bg>**\n\n%s\n",
pht('SURPLUS SCHEMATA'),
phutil_console_wrap($message));
} else if ($all_access) {
$console->writeOut(
"**<bg:yellow> %s </bg>**\n\n%s\n",
pht('ACCESS DENIED'),
phutil_console_wrap($message));
} else {
$console->writeOut(
"**<bg:red> %s </bg>**\n\n%s\n",
pht('SCHEMATA ERRORS'),
phutil_console_wrap($message));
}
return 2;
}
final protected function upgradeSchemata(
array $apis,
$apply_only = null,
$no_quickstart = false,
$init_only = false) {
$locks = array();
foreach ($apis as $api) {
$locks[] = $this->lock($api);
}
try {
$this->doUpgradeSchemata($apis, $apply_only, $no_quickstart, $init_only);
} catch (Exception $ex) {
foreach ($locks as $lock) {
$lock->unlock();
}
throw $ex;
}
foreach ($locks as $lock) {
$lock->unlock();
}
}
final private function doUpgradeSchemata(
array $apis,
$apply_only,
$no_quickstart,
$init_only) {
$patches = $this->patches;
$is_dryrun = $this->dryRun;
$api_map = array();
foreach ($apis as $api) {
$api_map[$api->getRef()->getRefKey()] = $api;
}
foreach ($api_map as $ref_key => $api) {
$applied = $api->getAppliedPatches();
$needs_init = ($applied === null);
if (!$needs_init) {
continue;
}
if ($is_dryrun) {
echo tsprintf(
"%s\n",
pht(
'DRYRUN: Storage on host "%s" does not exist yet, so it '.
'would be created.',
$ref_key));
continue;
}
if ($apply_only) {
throw new PhutilArgumentUsageException(
pht(
'Storage on host "%s" has not been initialized yet. You must '.
'initialize storage before selectively applying patches.',
$ref_key));
}
// If we're initializing storage for the first time on any host, track
// it so that we can give the user a nicer experience during the
// subsequent adjustment phase.
$this->didInitialize = true;
$legacy = $api->getLegacyPatches($patches);
if ($legacy || $no_quickstart || $init_only) {
// If we have legacy patches, we can't quickstart.
$api->createDatabase('meta_data');
$api->createTable(
'meta_data',
'patch_status',
array(
'patch VARCHAR(255) NOT NULL PRIMARY KEY COLLATE utf8_general_ci',
'applied INT UNSIGNED NOT NULL',
));
foreach ($legacy as $patch) {
$api->markPatchApplied($patch);
}
} else {
echo tsprintf(
"%s\n",
pht(
'Loading quickstart template onto "%s"...',
$ref_key));
$root = dirname(phutil_get_library_root('phabricator'));
$sql = $root.'/resources/sql/quickstart.sql';
$api->applyPatchSQL($sql);
}
}
if ($init_only) {
echo pht('Storage initialized.')."\n";
return 0;
}
$applied_map = array();
$state_map = array();
foreach ($api_map as $ref_key => $api) {
$applied = $api->getAppliedPatches();
// If we still have nothing applied, this is a dry run and we didn't
// actually initialize storage. Here, just do nothing.
if ($applied === null) {
if ($is_dryrun) {
continue;
} else {
throw new Exception(
pht(
'Database initialization on host "%s" applied no patches!',
$ref_key));
}
}
$applied = array_fuse($applied);
$state_map[$ref_key] = $applied;
if ($apply_only) {
if (isset($applied[$apply_only])) {
if (!$this->force && !$is_dryrun) {
echo phutil_console_wrap(
pht(
'Patch "%s" has already been applied on host "%s". Are you '.
'sure you want to apply it again? This may put your storage '.
'in a state that the upgrade scripts can not automatically '.
'manage.',
$apply_only,
$ref_key));
if (!phutil_console_confirm(pht('Apply patch again?'))) {
echo pht('Cancelled.')."\n";
return 1;
}
}
// Mark this patch as not yet applied on this host.
unset($applied[$apply_only]);
}
}
$applied_map[$ref_key] = $applied;
}
// If we're applying only a specific patch, select just that patch.
if ($apply_only) {
$patches = array_select_keys($patches, array($apply_only));
}
// Apply each patch to each database. We apply patches patch-by-patch,
// not database-by-database: for each patch we apply it to every database,
// then move to the next patch.
// We must do this because ".php" patches may depend on ".sql" patches
// being up to date on all masters, and that will work fine if we put each
// patch on every host before moving on. If we try to bring database hosts
// up to date one at a time we can end up in a big mess.
$duration_map = array();
// First, find any global patches which have been applied to ANY database.
// We are just going to mark these as applied without actually running
// them. Otherwise, adding new empty masters to an existing cluster will
// try to apply them against invalid states.
foreach ($patches as $key => $patch) {
if ($patch->getIsGlobalPatch()) {
foreach ($applied_map as $ref_key => $applied) {
if (isset($applied[$key])) {
$duration_map[$key] = 1;
}
}
}
}
while (true) {
$applied_something = false;
foreach ($patches as $key => $patch) {
// First, check if any databases need this patch. We can just skip it
// if it has already been applied everywhere.
$need_patch = array();
foreach ($applied_map as $ref_key => $applied) {
if (isset($applied[$key])) {
continue;
}
$need_patch[] = $ref_key;
}
if (!$need_patch) {
unset($patches[$key]);
continue;
}
// Check if we can apply this patch yet. Before we can apply a patch,
// all of the dependencies for the patch must have been applied on all
// databases. Requiring that all databases stay in sync prevents one
// database from racing ahead if it happens to get a patch that nothing
// else has yet.
$missing_patch = null;
foreach ($patch->getAfter() as $after) {
foreach ($applied_map as $ref_key => $applied) {
if (isset($applied[$after])) {
// This database already has the patch. We can apply it to
// other databases but don't need to apply it here.
continue;
}
$missing_patch = $after;
break 2;
}
}
if ($missing_patch) {
if ($apply_only) {
echo tsprintf(
"%s\n",
pht(
'Unable to apply patch "%s" because it depends on patch '.
'"%s", which has not been applied on some hosts: %s.',
$apply_only,
$missing_patch,
implode(', ', $need_patch)));
return 1;
} else {
// Some databases are missing the dependencies, so keep trying
// other patches instead. If everything goes right, we'll apply the
// dependencies and then come back and apply this patch later.
continue;
}
}
$is_global = $patch->getIsGlobalPatch();
$patch_apis = array_select_keys($api_map, $need_patch);
foreach ($patch_apis as $ref_key => $api) {
if ($is_global) {
// If this is a global patch which we previously applied, just
// read the duration from the map without actually applying
// the patch.
$duration = idx($duration_map, $key);
} else {
$duration = null;
}
if ($duration === null) {
if ($is_dryrun) {
echo tsprintf(
"%s\n",
pht(
'DRYRUN: Would apply patch "%s" to host "%s".',
$key,
$ref_key));
} else {
echo tsprintf(
"%s\n",
pht(
'Applying patch "%s" to host "%s"...',
$key,
$ref_key));
}
$t_begin = microtime(true);
if (!$is_dryrun) {
$api->applyPatch($patch);
}
$t_end = microtime(true);
$duration = ($t_end - $t_begin);
$duration_map[$key] = $duration;
}
// If we're explicitly reapplying this patch, we don't need to
// mark it as applied.
if (!isset($state_map[$ref_key][$key])) {
if (!$is_dryrun) {
$api->markPatchApplied($key, ($t_end - $t_begin));
}
$applied_map[$ref_key][$key] = true;
}
}
// We applied this everywhere, so we're done with the patch.
unset($patches[$key]);
$applied_something = true;
}
if (!$applied_something) {
if ($patches) {
throw new Exception(
pht(
'Some patches could not be applied: %s',
implode(', ', array_keys($patches))));
} else if (!$is_dryrun && !$apply_only) {
echo pht(
'Storage is up to date. Use "%s" for details.',
'storage status')."\n";
}
break;
}
}
}
final protected function getBareHostAndPort($host) {
// Split out port information, since the command-line client requires a
// separate flag for the port.
$uri = new PhutilURI('mysql://'.$host);
if ($uri->getPort()) {
$port = $uri->getPort();
$bare_hostname = $uri->getDomain();
} else {
$port = null;
$bare_hostname = $host;
}
return array($bare_hostname, $port);
}
/**
* Acquires a @{class:PhabricatorGlobalLock}.
*
* @return PhabricatorGlobalLock
*/
final protected function lock(PhabricatorStorageManagementAPI $api) {
// Although we're holding this lock on different databases so it could
// have the same name on each as far as the database is concerned, the
// locks would be the same within this process.
$parameters = array(
'refKey' => $api->getRef()->getRefKey(),
);
// We disable logging for this lock because we may not have created the
// log table yet, or may need to adjust it.
return PhabricatorGlobalLock::newLock('adjust', $parameters)
->useSpecificConnection($api->getConn(null))
->setDisableLogging(true)
->lock();
}
final protected function analyzeTables(
PhabricatorStorageManagementAPI $api) {
// Analyzing tables can sometimes have a significant effect on query
// performance, particularly for the fulltext ngrams tables. See T12819
// for some specific examples.
$conn = $api->getConn(null);
$patches = $this->getPatches();
$databases = $api->getDatabaseList($patches, true);
$this->logInfo(
pht('ANALYZE'),
pht('Analyzing tables...'));
$targets = array();
foreach ($databases as $database) {
queryfx($conn, 'USE %C', $database);
$tables = queryfx_all($conn, 'SHOW TABLE STATUS');
foreach ($tables as $table) {
$table_name = $table['Name'];
$targets[] = array(
'database' => $database,
'table' => $table_name,
);
}
}
$bar = id(new PhutilConsoleProgressBar())
->setTotal(count($targets));
foreach ($targets as $target) {
queryfx(
$conn,
'ANALYZE TABLE %T.%T',
$target['database'],
$target['table']);
$bar->update(1);
}
$bar->done();
$this->logOkay(
pht('ANALYZED'),
pht(
'Analyzed %d table(s).',
count($targets)));
}
}
diff --git a/src/infrastructure/util/PhabricatorMetronome.php b/src/infrastructure/util/PhabricatorMetronome.php
new file mode 100644
index 000000000..24f58127f
--- /dev/null
+++ b/src/infrastructure/util/PhabricatorMetronome.php
@@ -0,0 +1,92 @@
+<?php
+
+/**
+ * Tick at a given frequency with a specifiable offset.
+ *
+ * One use case for this is to flatten out load spikes caused by periodic
+ * service calls. Give each host a metronome that ticks at the same frequency,
+ * but with different offsets. Then, have hosts make service calls only after
+ * their metronome ticks. This spreads service calls out evenly more quickly
+ * and more predictably than adding random jitter.
+ */
+final class PhabricatorMetronome
+ extends Phobject {
+
+ private $offset = 0;
+ private $frequency;
+
+ public function setOffset($offset) {
+ if (!is_int($offset)) {
+ throw new Exception(pht('Metronome offset must be an integer.'));
+ }
+
+ if ($offset < 0) {
+ throw new Exception(pht('Metronome offset must be 0 or more.'));
+ }
+
+ // We're not requiring that the offset be smaller than the frequency. If
+ // the offset is larger, we'll just clamp it to the frequency before we
+ // use it. This allows the offset to be configured before the frequency
+ // is configured, which is useful for using a hostname as an offset seed.
+
+ $this->offset = $offset;
+
+ return $this;
+ }
+
+ public function setFrequency($frequency) {
+ if (!is_int($frequency)) {
+ throw new Exception(pht('Metronome frequency must be an integer.'));
+ }
+
+ if ($frequency < 1) {
+ throw new Exception(pht('Metronome frequency must be 1 or more.'));
+ }
+
+ $this->frequency = $frequency;
+
+ return $this;
+ }
+
+ public function setOffsetFromSeed($seed) {
+ $offset = PhabricatorHash::digestToRange($seed, 0, PHP_INT_MAX);
+ return $this->setOffset($offset);
+ }
+
+ public function getFrequency() {
+ if ($this->frequency === null) {
+ throw new PhutilInvalidStateException('setFrequency');
+ }
+ return $this->frequency;
+ }
+
+ public function getOffset() {
+ $frequency = $this->getFrequency();
+ return ($this->offset % $frequency);
+ }
+
+ public function getNextTickAfter($epoch) {
+ $frequency = $this->getFrequency();
+ $offset = $this->getOffset();
+
+ $remainder = ($epoch % $frequency);
+
+ if ($remainder < $offset) {
+ return ($epoch - $remainder) + $offset;
+ } else {
+ return ($epoch - $remainder) + $frequency + $offset;
+ }
+ }
+
+ public function didTickBetween($min, $max) {
+ if ($max < $min) {
+ throw new Exception(
+ pht(
+ 'Maximum tick window must not be smaller than minimum tick window.'));
+ }
+
+ $next = $this->getNextTickAfter($min);
+ return ($next <= $max);
+ }
+
+}
diff --git a/src/infrastructure/util/__tests__/PhabricatorMetronomeTestCase.php b/src/infrastructure/util/__tests__/PhabricatorMetronomeTestCase.php
new file mode 100644
index 000000000..9ad74e2b9
--- /dev/null
+++ b/src/infrastructure/util/__tests__/PhabricatorMetronomeTestCase.php
@@ -0,0 +1,61 @@
+<?php
+
+final class PhabricatorMetronomeTestCase
+ extends PhabricatorTestCase {
+
+ public function testMetronomeOffsets() {
+ $cases = array(
+ 'web001.example.net' => 44,
+ 'web002.example.net' => 36,
+ 'web003.example.net' => 25,
+ 'web004.example.net' => 25,
+ 'web005.example.net' => 16,
+ 'web006.example.net' => 26,
+ 'web007.example.net' => 35,
+ 'web008.example.net' => 14,
+ );
+
+ $metronome = id(new PhabricatorMetronome())
+ ->setFrequency(60);
+
+ foreach ($cases as $input => $expect) {
+ $metronome->setOffsetFromSeed($input);
+
+ $this->assertEqual(
+ $expect,
+ $metronome->getOffset(),
+ pht('Offset for: %s', $input));
+ }
+ }
+
+ public function testMetronomeTicks() {
+ $metronome = id(new PhabricatorMetronome())
+ ->setFrequency(60)
+ ->setOffset(13);
+
+ $tick_epoch = strtotime('2000-01-01 11:11:13 AM UTC');
+
+ // Since the epoch is at "0:13" on the clock, the metronome should tick
+ // then.
+ $this->assertEqual(
+ $tick_epoch,
+ $metronome->getNextTickAfter($tick_epoch - 1),
+ pht('Tick at 11:11:13 AM.'));
+
+ // The next tick should be a minute later.
+ $this->assertEqual(
+ $tick_epoch + 60,
+ $metronome->getNextTickAfter($tick_epoch),
+ pht('Tick at 11:12:13 AM.'));
+
+
+ // There's no tick in the next 59 seconds.
+ $this->assertFalse(
+ $metronome->didTickBetween($tick_epoch, $tick_epoch + 59));
+
+ $this->assertTrue(
+ $metronome->didTickBetween($tick_epoch, $tick_epoch + 60));
+ }
+
+
+}
diff --git a/src/view/control/AphrontCursorPagerView.php b/src/view/control/AphrontCursorPagerView.php
index 460815848..cdb956262 100644
--- a/src/view/control/AphrontCursorPagerView.php
+++ b/src/view/control/AphrontCursorPagerView.php
@@ -1,189 +1,189 @@
<?php
final class AphrontCursorPagerView extends AphrontView {
private $afterID;
private $beforeID;
private $pageSize = 100;
private $nextPageID;
private $prevPageID;
private $moreResults;
private $uri;
public function setPageSize($page_size) {
$this->pageSize = max(1, $page_size);
return $this;
}
public function getPageSize() {
return $this->pageSize;
}
public function setURI(PhutilURI $uri) {
$this->uri = $uri;
return $this;
}
public function readFromRequest(AphrontRequest $request) {
$this->uri = $request->getRequestURI();
$this->afterID = $request->getStr('after');
$this->beforeID = $request->getStr('before');
return $this;
}
public function setAfterID($after_id) {
$this->afterID = $after_id;
return $this;
}
public function getAfterID() {
return $this->afterID;
}
public function setBeforeID($before_id) {
$this->beforeID = $before_id;
return $this;
}
public function getBeforeID() {
return $this->beforeID;
}
public function setNextPageID($next_page_id) {
$this->nextPageID = $next_page_id;
return $this;
}
public function getNextPageID() {
return $this->nextPageID;
}
public function setPrevPageID($prev_page_id) {
$this->prevPageID = $prev_page_id;
return $this;
}
public function getPrevPageID() {
return $this->prevPageID;
}
public function sliceResults(array $results) {
if (count($results) > $this->getPageSize()) {
$offset = ($this->beforeID ? count($results) - $this->getPageSize() : 0);
$results = array_slice($results, $offset, $this->getPageSize(), true);
$this->moreResults = true;
}
return $results;
}
public function getHasMoreResults() {
return $this->moreResults;
}
public function willShowPagingControls() {
return $this->prevPageID ||
$this->nextPageID ||
$this->afterID ||
($this->beforeID && $this->moreResults);
}
public function getFirstPageURI() {
if (!$this->uri) {
throw new PhutilInvalidStateException('setURI');
}
if (!$this->afterID && !($this->beforeID && $this->moreResults)) {
return null;
}
- return $this->uri
- ->alter('before', null)
- ->alter('after', null);
+ return id(clone $this->uri)
+ ->removeQueryParam('after')
+ ->removeQueryParam('before');
}
public function getPrevPageURI() {
if (!$this->uri) {
throw new PhutilInvalidStateException('getPrevPageURI');
}
if (!$this->prevPageID) {
return null;
}
- return $this->uri
- ->alter('after', null)
- ->alter('before', $this->prevPageID);
+ return id(clone $this->uri)
+ ->removeQueryParam('after')
+ ->replaceQueryParam('before', $this->prevPageID);
}
public function getNextPageURI() {
if (!$this->uri) {
throw new PhutilInvalidStateException('setURI');
}
if (!$this->nextPageID) {
return null;
}
- return $this->uri
- ->alter('after', $this->nextPageID)
- ->alter('before', null);
+ return id(clone $this->uri)
+ ->replaceQueryParam('after', $this->nextPageID)
+ ->removeQueryParam('before');
}
public function render() {
if (!$this->uri) {
throw new PhutilInvalidStateException('setURI');
}
$links = array();
$first_uri = $this->getFirstPageURI();
if ($first_uri) {
$icon = id(new PHUIIconView())
->setIcon('fa-fast-backward');
$links[] = id(new PHUIButtonView())
->setTag('a')
->setHref($first_uri)
->setIcon($icon)
->addClass('mml')
->setColor(PHUIButtonView::GREY)
->setText(pht('First'));
}
$prev_uri = $this->getPrevPageURI();
if ($prev_uri) {
$icon = id(new PHUIIconView())
->setIcon('fa-backward');
$links[] = id(new PHUIButtonView())
->setTag('a')
->setHref($prev_uri)
->setIcon($icon)
->addClass('mml')
->setColor(PHUIButtonView::GREY)
->setText(pht('Prev'));
}
$next_uri = $this->getNextPageURI();
if ($next_uri) {
$icon = id(new PHUIIconView())
->setIcon('fa-forward');
$links[] = id(new PHUIButtonView())
->setTag('a')
->setHref($next_uri)
->setIcon($icon, false)
->addClass('mml')
->setColor(PHUIButtonView::GREY)
->setText(pht('Next'));
}
return phutil_tag(
'div',
array(
'class' => 'phui-pager-view',
),
$links);
}
}
diff --git a/src/view/form/control/AphrontFormTextControl.php b/src/view/form/control/AphrontFormTextControl.php
index 581f22682..f7fd117cf 100644
--- a/src/view/form/control/AphrontFormTextControl.php
+++ b/src/view/form/control/AphrontFormTextControl.php
@@ -1,55 +1,66 @@
<?php
final class AphrontFormTextControl extends AphrontFormControl {
private $disableAutocomplete;
private $sigil;
private $placeholder;
+ private $autofocus;
public function setDisableAutocomplete($disable) {
$this->disableAutocomplete = $disable;
return $this;
}
private function getDisableAutocomplete() {
return $this->disableAutocomplete;
}
public function getPlaceholder() {
return $this->placeholder;
}
public function setPlaceholder($placeholder) {
$this->placeholder = $placeholder;
return $this;
}
+ public function setAutofocus($autofocus) {
+ $this->autofocus = $autofocus;
+ return $this;
+ }
+
+ public function getAutofocus() {
+ return $this->autofocus;
+ }
+
public function getSigil() {
return $this->sigil;
}
public function setSigil($sigil) {
$this->sigil = $sigil;
return $this;
}
protected function getCustomControlClass() {
return 'aphront-form-control-text';
}
protected function renderInput() {
return javelin_tag(
'input',
array(
'type' => 'text',
'name' => $this->getName(),
'value' => $this->getValue(),
'disabled' => $this->getDisabled() ? 'disabled' : null,
'autocomplete' => $this->getDisableAutocomplete() ? 'off' : null,
'id' => $this->getID(),
'sigil' => $this->getSigil(),
'placeholder' => $this->getPlaceholder(),
+ 'autofocus' => ($this->getAutofocus() ? 'autofocus' : null),
));
}
}
diff --git a/src/view/form/control/AphrontFormTokenizerControl.php b/src/view/form/control/AphrontFormTokenizerControl.php
index 3d65c4e52..fe80c86f8 100644
--- a/src/view/form/control/AphrontFormTokenizerControl.php
+++ b/src/view/form/control/AphrontFormTokenizerControl.php
@@ -1,150 +1,154 @@
<?php
final class AphrontFormTokenizerControl extends AphrontFormControl {
private $datasource;
private $disableBehavior;
private $limit;
private $placeholder;
private $handles;
private $initialValue;
public function setDatasource(PhabricatorTypeaheadDatasource $datasource) {
$this->datasource = $datasource;
return $this;
}
public function setDisableBehavior($disable) {
$this->disableBehavior = $disable;
return $this;
}
protected function getCustomControlClass() {
return 'aphront-form-control-tokenizer';
}
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
public function setPlaceholder($placeholder) {
$this->placeholder = $placeholder;
return $this;
}
public function setInitialValue(array $initial_value) {
$this->initialValue = $initial_value;
return $this;
}
public function getInitialValue() {
return $this->initialValue;
}
public function willRender() {
// Load the handles now so we'll get a bulk load later on when we actually
// render them.
$this->loadHandles();
}
protected function renderInput() {
$name = $this->getName();
$handles = $this->loadHandles();
$handles = iterator_to_array($handles);
if ($this->getID()) {
$id = $this->getID();
} else {
$id = celerity_generate_unique_node_id();
}
$datasource = $this->datasource;
if (!$datasource) {
throw new Exception(
pht('You must set a datasource to use a TokenizerControl.'));
}
$datasource->setViewer($this->getUser());
$placeholder = null;
if (!strlen($this->placeholder)) {
$placeholder = $datasource->getPlaceholderText();
}
$values = nonempty($this->getValue(), array());
$tokens = $datasource->renderTokens($values);
foreach ($tokens as $token) {
$token->setInputName($this->getName());
}
$template = id(new AphrontTokenizerTemplateView())
->setName($name)
->setID($id)
->setValue($tokens);
$initial_value = $this->getInitialValue();
if ($initial_value !== null) {
$template->setInitialValue($initial_value);
}
$username = null;
if ($this->hasViewer()) {
$username = $this->getViewer()->getUsername();
}
$datasource_uri = $datasource->getDatasourceURI();
$browse_uri = $datasource->getBrowseURI();
if ($browse_uri) {
$template->setBrowseURI($browse_uri);
}
if (!$this->disableBehavior) {
Javelin::initBehavior('aphront-basic-tokenizer', array(
'id' => $id,
'src' => $datasource_uri,
'value' => mpull($tokens, 'getValue', 'getKey'),
'icons' => mpull($tokens, 'getIcon', 'getKey'),
'types' => mpull($tokens, 'getTokenType', 'getKey'),
'colors' => mpull($tokens, 'getColor', 'getKey'),
+ 'availabilityColors' => mpull(
+ $tokens,
+ 'getAvailabilityColor',
+ 'getKey'),
'limit' => $this->limit,
'username' => $username,
'placeholder' => $placeholder,
'browseURI' => $browse_uri,
'disabled' => $this->getDisabled(),
));
}
return $template->render();
}
private function loadHandles() {
if ($this->handles === null) {
$viewer = $this->getUser();
if (!$viewer) {
throw new Exception(
pht(
'Call %s before rendering tokenizers. '.
'Use %s on %s to do this easily.',
'setUser()',
'appendControl()',
'AphrontFormView'));
}
$values = nonempty($this->getValue(), array());
$phids = array();
foreach ($values as $value) {
if (!PhabricatorTypeaheadDatasource::isFunctionToken($value)) {
$phids[] = $value;
}
}
$this->handles = $viewer->loadHandles($phids);
}
return $this->handles;
}
}
diff --git a/src/view/form/control/PHUIFormNumberControl.php b/src/view/form/control/PHUIFormNumberControl.php
index 26e7e0395..c577bebbd 100644
--- a/src/view/form/control/PHUIFormNumberControl.php
+++ b/src/view/form/control/PHUIFormNumberControl.php
@@ -1,40 +1,51 @@
<?php
final class PHUIFormNumberControl extends AphrontFormControl {
private $disableAutocomplete;
+ private $autofocus;
public function setDisableAutocomplete($disable_autocomplete) {
$this->disableAutocomplete = $disable_autocomplete;
return $this;
}
public function getDisableAutocomplete() {
return $this->disableAutocomplete;
}
+ public function setAutofocus($autofocus) {
+ $this->autofocus = $autofocus;
+ return $this;
+ }
+
+ public function getAutofocus() {
+ return $this->autofocus;
+ }
+
protected function getCustomControlClass() {
return 'phui-form-number';
}
protected function renderInput() {
if ($this->getDisableAutocomplete()) {
$autocomplete = 'off';
} else {
$autocomplete = null;
}
return javelin_tag(
'input',
array(
'type' => 'text',
'pattern' => '\d*',
'name' => $this->getName(),
'value' => $this->getValue(),
'disabled' => $this->getDisabled() ? 'disabled' : null,
'autocomplete' => $autocomplete,
'id' => $this->getID(),
+ 'autofocus' => ($this->getAutofocus() ? 'autofocus' : null),
));
}
}
diff --git a/src/view/form/control/PHUIFormTimerControl.php b/src/view/form/control/PHUIFormTimerControl.php
index 7229d649e..090de2c8e 100644
--- a/src/view/form/control/PHUIFormTimerControl.php
+++ b/src/view/form/control/PHUIFormTimerControl.php
@@ -1,40 +1,68 @@
<?php
final class PHUIFormTimerControl extends AphrontFormControl {
private $icon;
+ private $updateURI;
public function setIcon(PHUIIconView $icon) {
$this->icon = $icon;
return $this;
}
public function getIcon() {
return $this->icon;
}
+ public function setUpdateURI($update_uri) {
+ $this->updateURI = $update_uri;
+ return $this;
+ }
+
+ public function getUpdateURI() {
+ return $this->updateURI;
+ }
+
protected function getCustomControlClass() {
return 'phui-form-timer';
}
protected function renderInput() {
+ return $this->newTimerView();
+ }
+
+ public function newTimerView() {
$icon_cell = phutil_tag(
'td',
array(
'class' => 'phui-form-timer-icon',
),
$this->getIcon());
$content_cell = phutil_tag(
'td',
array(
'class' => 'phui-form-timer-content',
),
$this->renderChildren());
$row = phutil_tag('tr', array(), array($icon_cell, $content_cell));
- return phutil_tag('table', array(), $row);
+ $node_id = null;
+
+ $update_uri = $this->getUpdateURI();
+ if ($update_uri) {
+ $node_id = celerity_generate_unique_node_id();
+
+ Javelin::initBehavior(
+ 'phui-timer-control',
+ array(
+ 'nodeID' => $node_id,
+ 'uri' => $update_uri,
+ ));
+ }
+
+ return phutil_tag('table', array('id' => $node_id), $row);
}
}
diff --git a/src/view/layout/PhabricatorActionView.php b/src/view/layout/PhabricatorActionView.php
index a1d8fe266..3de60a237 100644
--- a/src/view/layout/PhabricatorActionView.php
+++ b/src/view/layout/PhabricatorActionView.php
@@ -1,374 +1,375 @@
<?php
final class PhabricatorActionView extends AphrontView {
private $name;
private $icon;
private $href;
private $disabled;
private $label;
private $workflow;
private $renderAsForm;
private $download;
private $sigils = array();
private $metadata;
private $selected;
private $openInNewWindow;
private $submenu = array();
private $hidden;
private $depth;
private $id;
private $order;
private $color;
private $type;
const TYPE_DIVIDER = 'type-divider';
const TYPE_LABEL = 'label';
const RED = 'action-item-red';
+ const GREEN = 'action-item-green';
public function setSelected($selected) {
$this->selected = $selected;
return $this;
}
public function getSelected() {
return $this->selected;
}
public function setMetadata($metadata) {
$this->metadata = $metadata;
return $this;
}
public function getMetadata() {
return $this->metadata;
}
public function setDownload($download) {
$this->download = $download;
return $this;
}
public function getDownload() {
return $this->download;
}
public function setHref($href) {
$this->href = $href;
return $this;
}
public function setColor($color) {
$this->color = $color;
return $this;
}
public function addSigil($sigil) {
$this->sigils[] = $sigil;
return $this;
}
public function getHref() {
return $this->href;
}
public function setIcon($icon) {
$this->icon = $icon;
return $this;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setLabel($label) {
$this->label = $label;
return $this;
}
public function setDisabled($disabled) {
$this->disabled = $disabled;
return $this;
}
public function getDisabled() {
return $this->disabled;
}
public function setWorkflow($workflow) {
$this->workflow = $workflow;
return $this;
}
public function setRenderAsForm($form) {
$this->renderAsForm = $form;
return $this;
}
public function setOpenInNewWindow($open_in_new_window) {
$this->openInNewWindow = $open_in_new_window;
return $this;
}
public function getOpenInNewWindow() {
return $this->openInNewWindow;
}
public function setID($id) {
$this->id = $id;
return $this;
}
public function getID() {
if (!$this->id) {
$this->id = celerity_generate_unique_node_id();
}
return $this->id;
}
public function setOrder($order) {
$this->order = $order;
return $this;
}
public function getOrder() {
return $this->order;
}
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
public function setSubmenu(array $submenu) {
$this->submenu = $submenu;
if (!$this->getHref()) {
$this->setHref('#');
}
return $this;
}
public function getItems($depth = 0) {
$items = array();
$items[] = $this;
foreach ($this->submenu as $action) {
foreach ($action->getItems($depth + 1) as $item) {
$item
->setHidden(true)
->setDepth($depth + 1);
$items[] = $item;
}
}
return $items;
}
public function setHidden($hidden) {
$this->hidden = $hidden;
return $this;
}
public function getHidden() {
return $this->hidden;
}
public function setDepth($depth) {
$this->depth = $depth;
return $this;
}
public function getDepth() {
return $this->depth;
}
public function render() {
$caret_id = celerity_generate_unique_node_id();
$icon = null;
if ($this->icon) {
$color = '';
if ($this->disabled) {
$color = ' grey';
}
$icon = id(new PHUIIconView())
->addClass('phabricator-action-view-icon')
->setIcon($this->icon.$color);
}
$sigils = array();
if ($this->workflow) {
$sigils[] = 'workflow';
}
if ($this->download) {
$sigils[] = 'download';
}
if ($this->submenu) {
$sigils[] = 'keep-open';
}
if ($this->sigils) {
$sigils = array_merge($sigils, $this->sigils);
}
$sigils = $sigils ? implode(' ', $sigils) : null;
if ($this->href) {
if ($this->renderAsForm) {
if (!$this->hasViewer()) {
throw new Exception(
pht(
'Call %s when rendering an action as a form.',
'setViewer()'));
}
$item = javelin_tag(
'button',
array(
'class' => 'phabricator-action-view-item',
),
array($icon, $this->name));
$item = phabricator_form(
$this->getViewer(),
array(
'action' => $this->getHref(),
'method' => 'POST',
'sigil' => $sigils,
'meta' => $this->metadata,
),
$item);
} else {
if ($this->getOpenInNewWindow()) {
$target = '_blank';
$rel = 'noreferrer';
} else {
$target = null;
$rel = null;
}
if ($this->submenu) {
$caret = javelin_tag(
'span',
array(
'class' => 'caret-right',
'id' => $caret_id,
),
'');
} else {
$caret = null;
}
$item = javelin_tag(
'a',
array(
'href' => $this->getHref(),
'class' => 'phabricator-action-view-item',
'target' => $target,
'rel' => $rel,
'sigil' => $sigils,
'meta' => $this->metadata,
),
array($icon, $this->name, $caret));
}
} else {
$item = javelin_tag(
'span',
array(
'class' => 'phabricator-action-view-item',
'sigil' => $sigils,
),
array($icon, $this->name, $this->renderChildren()));
}
$classes = array();
$classes[] = 'phabricator-action-view';
if ($this->disabled) {
$classes[] = 'phabricator-action-view-disabled';
}
if ($this->label) {
$classes[] = 'phabricator-action-view-label';
}
if ($this->selected) {
$classes[] = 'phabricator-action-view-selected';
}
if ($this->submenu) {
$classes[] = 'phabricator-action-view-submenu';
}
if ($this->getHref()) {
$classes[] = 'phabricator-action-view-href';
}
if ($this->icon) {
$classes[] = 'action-has-icon';
}
if ($this->color) {
$classes[] = $this->color;
}
if ($this->type) {
$classes[] = 'phabricator-action-view-'.$this->type;
}
$style = array();
if ($this->hidden) {
$style[] = 'display: none;';
}
if ($this->depth) {
$indent = ($this->depth * 16);
$style[] = "margin-left: {$indent}px;";
}
$sigil = null;
$meta = null;
if ($this->submenu) {
Javelin::initBehavior('phui-submenu');
$sigil = 'phui-submenu';
$item_ids = array();
foreach ($this->submenu as $subitem) {
$item_ids[] = $subitem->getID();
}
$meta = array(
'itemIDs' => $item_ids,
'caretID' => $caret_id,
);
}
return javelin_tag(
'li',
array(
'id' => $this->getID(),
'class' => implode(' ', $classes),
'style' => implode(' ', $style),
'sigil' => $sigil,
'meta' => $meta,
),
$item);
}
}
diff --git a/src/view/phui/PHUIHeadThingView.php b/src/view/phui/PHUIHeadThingView.php
index ab2feee98..a8498dcc5 100644
--- a/src/view/phui/PHUIHeadThingView.php
+++ b/src/view/phui/PHUIHeadThingView.php
@@ -1,71 +1,72 @@
<?php
final class PHUIHeadThingView extends AphrontTagView {
private $image;
private $imageHref;
private $content;
private $size;
const SMALL = 'head-thing-small';
const MEDIUM = 'head-thing-medium';
public function setImageHref($href) {
$this->imageHref = $href;
return $this;
}
public function setImage($image) {
$this->image = $image;
return $this;
}
public function setContent($content) {
$this->content = $content;
return $this;
}
public function setSize($size) {
$this->size = $size;
return $this;
}
protected function getTagAttributes() {
require_celerity_resource('phui-head-thing-view-css');
$classes = array();
$classes[] = 'phui-head-thing-view';
if ($this->image) {
$classes[] = 'phui-head-has-image';
}
if ($this->size) {
$classes[] = $this->size;
} else {
$classes[] = self::SMALL;
}
return array(
'class' => $classes,
);
}
protected function getTagContent() {
- $image = phutil_tag(
+ $image = javelin_tag(
'a',
array(
'class' => 'phui-head-thing-image visual-only',
'style' => 'background-image: url('.$this->image.');',
'href' => $this->imageHref,
+ 'aural' => false,
));
if ($this->image) {
return array($image, $this->content);
} else {
return $this->content;
}
}
}
diff --git a/src/view/phui/PHUIIconView.php b/src/view/phui/PHUIIconView.php
index 8cc61ba2e..d907cb334 100644
--- a/src/view/phui/PHUIIconView.php
+++ b/src/view/phui/PHUIIconView.php
@@ -1,888 +1,902 @@
<?php
final class PHUIIconView extends AphrontTagView {
const SPRITE_TOKENS = 'tokens';
const SPRITE_LOGIN = 'login';
const HEAD_SMALL = 'phuihead-small';
const HEAD_MEDIUM = 'phuihead-medium';
private $href = null;
private $image;
private $text;
private $headSize = null;
private $spriteIcon;
private $spriteSheet;
private $iconFont;
private $iconColor;
private $iconBackground;
private $tooltip;
+ private $emblemColor;
public function setHref($href) {
$this->href = $href;
return $this;
}
public function setImage($image) {
$this->image = $image;
return $this;
}
public function setText($text) {
$this->text = $text;
return $this;
}
public function setHeadSize($size) {
$this->headSize = $size;
return $this;
}
public function setSpriteIcon($sprite) {
$this->spriteIcon = $sprite;
return $this;
}
public function setSpriteSheet($sheet) {
$this->spriteSheet = $sheet;
return $this;
}
public function setIcon($icon, $color = null) {
$this->iconFont = $icon;
$this->iconColor = $color;
return $this;
}
public function setBackground($color) {
$this->iconBackground = $color;
return $this;
}
public function setTooltip($text) {
$this->tooltip = $text;
return $this;
}
+ public function setEmblemColor($emblem_color) {
+ $this->emblemColor = $emblem_color;
+ return $this;
+ }
+
+ public function getEmblemColor() {
+ return $this->emblemColor;
+ }
+
protected function getTagName() {
$tag = 'span';
if ($this->href) {
$tag = 'a';
}
return $tag;
}
protected function getTagAttributes() {
require_celerity_resource('phui-icon-view-css');
$style = null;
$classes = array();
$classes[] = 'phui-icon-view';
if ($this->spriteIcon) {
require_celerity_resource('sprite-'.$this->spriteSheet.'-css');
$classes[] = 'sprite-'.$this->spriteSheet;
$classes[] = $this->spriteSheet.'-'.$this->spriteIcon;
} else if ($this->iconFont) {
require_celerity_resource('phui-font-icon-base-css');
require_celerity_resource('font-fontawesome');
$classes[] = 'phui-font-fa';
$classes[] = $this->iconFont;
if ($this->iconColor) {
$classes[] = $this->iconColor;
}
if ($this->iconBackground) {
$classes[] = 'phui-icon-square';
$classes[] = $this->iconBackground;
}
} else {
if ($this->headSize) {
$classes[] = $this->headSize;
}
$style = 'background-image: url('.$this->image.');';
}
if ($this->text) {
$classes[] = 'phui-icon-has-text';
$this->appendChild($this->text);
}
+ if ($this->emblemColor) {
+ $classes[] = 'phui-icon-emblem phui-icon-emblem-'.$this->emblemColor;
+ }
+
$sigil = null;
$meta = array();
if ($this->tooltip) {
Javelin::initBehavior('phabricator-tooltips');
require_celerity_resource('aphront-tooltip-css');
$sigil = 'has-tooltip';
$meta = array(
'tip' => $this->tooltip,
);
}
return array(
'href' => $this->href,
'style' => $style,
'aural' => false,
'class' => $classes,
'sigil' => $sigil,
'meta' => $meta,
);
}
public static function getSheetManifest($sheet) {
$root = dirname(phutil_get_library_root('phabricator'));
$path = $root.'/resources/sprite/manifest/'.$sheet.'.json';
$data = Filesystem::readFile($path);
return idx(phutil_json_decode($data), 'sprites');
}
public static function getIcons() {
return array(
'fa-glass',
'fa-music',
'fa-search',
'fa-envelope-o',
'fa-heart',
'fa-star',
'fa-star-o',
'fa-user',
'fa-film',
'fa-th-large',
'fa-th',
'fa-th-list',
'fa-check',
'fa-times',
'fa-search-plus',
'fa-search-minus',
'fa-power-off',
'fa-signal',
'fa-cog',
'fa-trash-o',
'fa-home',
'fa-file-o',
'fa-clock-o',
'fa-road',
'fa-download',
'fa-arrow-circle-o-down',
'fa-arrow-circle-o-up',
'fa-inbox',
'fa-play-circle-o',
'fa-repeat',
'fa-refresh',
'fa-list-alt',
'fa-lock',
'fa-flag',
'fa-headphones',
'fa-volume-off',
'fa-volume-down',
'fa-volume-up',
'fa-qrcode',
'fa-barcode',
'fa-tag',
'fa-tags',
'fa-book',
'fa-bookmark',
'fa-print',
'fa-camera',
'fa-font',
'fa-bold',
'fa-italic',
'fa-text-height',
'fa-text-width',
'fa-align-left',
'fa-align-center',
'fa-align-right',
'fa-align-justify',
'fa-list',
'fa-outdent',
'fa-indent',
'fa-video-camera',
'fa-picture-o',
'fa-pencil',
'fa-map-marker',
'fa-adjust',
'fa-tint',
'fa-pencil-square-o',
'fa-share-square-o',
'fa-check-square-o',
'fa-arrows',
'fa-step-backward',
'fa-fast-backward',
'fa-backward',
'fa-play',
'fa-pause',
'fa-stop',
'fa-forward',
'fa-fast-forward',
'fa-step-forward',
'fa-eject',
'fa-chevron-left',
'fa-chevron-right',
'fa-plus-circle',
'fa-minus-circle',
'fa-times-circle',
'fa-check-circle',
'fa-question-circle',
'fa-info-circle',
'fa-crosshairs',
'fa-times-circle-o',
'fa-check-circle-o',
'fa-ban',
'fa-arrow-left',
'fa-arrow-right',
'fa-arrow-up',
'fa-arrow-down',
'fa-share',
'fa-expand',
'fa-compress',
'fa-plus',
'fa-minus',
'fa-asterisk',
'fa-exclamation-circle',
'fa-gift',
'fa-leaf',
'fa-fire',
'fa-eye',
'fa-eye-slash',
'fa-exclamation-triangle',
'fa-plane',
'fa-calendar',
'fa-random',
'fa-comment',
'fa-magnet',
'fa-chevron-up',
'fa-chevron-down',
'fa-retweet',
'fa-shopping-cart',
'fa-folder',
'fa-folder-open',
'fa-arrows-v',
'fa-arrows-h',
'fa-bar-chart-o',
'fa-twitter-square',
'fa-facebook-square',
'fa-camera-retro',
'fa-key',
'fa-cogs',
'fa-comments',
'fa-thumbs-o-up',
'fa-thumbs-o-down',
'fa-star-half',
'fa-heart-o',
'fa-sign-out',
'fa-linkedin-square',
'fa-thumb-tack',
'fa-external-link',
'fa-sign-in',
'fa-trophy',
'fa-github-square',
'fa-upload',
'fa-lemon-o',
'fa-phone',
'fa-square-o',
'fa-bookmark-o',
'fa-phone-square',
'fa-twitter',
'fa-facebook',
'fa-github',
'fa-unlock',
'fa-credit-card',
'fa-rss',
'fa-hdd-o',
'fa-bullhorn',
'fa-bell',
'fa-certificate',
'fa-hand-o-right',
'fa-hand-o-left',
'fa-hand-o-up',
'fa-hand-o-down',
'fa-arrow-circle-left',
'fa-arrow-circle-right',
'fa-arrow-circle-up',
'fa-arrow-circle-down',
'fa-globe',
'fa-wrench',
'fa-tasks',
'fa-filter',
'fa-briefcase',
'fa-arrows-alt',
'fa-users',
'fa-link',
'fa-cloud',
'fa-flask',
'fa-scissors',
'fa-files-o',
'fa-paperclip',
'fa-floppy-o',
'fa-square',
'fa-bars',
'fa-list-ul',
'fa-list-ol',
'fa-strikethrough',
'fa-underline',
'fa-table',
'fa-magic',
'fa-truck',
'fa-pinterest',
'fa-pinterest-square',
'fa-google-plus-square',
'fa-google-plus',
'fa-money',
'fa-caret-down',
'fa-caret-up',
'fa-caret-left',
'fa-caret-right',
'fa-columns',
'fa-sort',
'fa-sort-asc',
'fa-sort-desc',
'fa-envelope',
'fa-linkedin',
'fa-undo',
'fa-gavel',
'fa-tachometer',
'fa-comment-o',
'fa-comments-o',
'fa-bolt',
'fa-sitemap',
'fa-umbrella',
'fa-clipboard',
'fa-lightbulb-o',
'fa-exchange',
'fa-cloud-download',
'fa-cloud-upload',
'fa-user-md',
'fa-stethoscope',
'fa-suitcase',
'fa-bell-o',
'fa-coffee',
'fa-cutlery',
'fa-file-text-o',
'fa-building-o',
'fa-hospital-o',
'fa-ambulance',
'fa-medkit',
'fa-fighter-jet',
'fa-beer',
'fa-h-square',
'fa-plus-square',
'fa-angle-double-left',
'fa-angle-double-right',
'fa-angle-double-up',
'fa-angle-double-down',
'fa-angle-left',
'fa-angle-right',
'fa-angle-up',
'fa-angle-down',
'fa-desktop',
'fa-laptop',
'fa-tablet',
'fa-mobile',
'fa-circle-o',
'fa-quote-left',
'fa-quote-right',
'fa-spinner',
'fa-circle',
'fa-reply',
'fa-github-alt',
'fa-folder-o',
'fa-folder-open-o',
'fa-smile-o',
'fa-frown-o',
'fa-meh-o',
'fa-gamepad',
'fa-keyboard-o',
'fa-flag-o',
'fa-flag-checkered',
'fa-terminal',
'fa-code',
'fa-reply-all',
'fa-mail-reply-all',
'fa-star-half-o',
'fa-location-arrow',
'fa-crop',
'fa-code-fork',
'fa-chain-broken',
'fa-question',
'fa-info',
'fa-exclamation',
'fa-superscript',
'fa-subscript',
'fa-eraser',
'fa-puzzle-piece',
'fa-microphone',
'fa-microphone-slash',
'fa-shield',
'fa-calendar-o',
'fa-fire-extinguisher',
'fa-rocket',
'fa-maxcdn',
'fa-chevron-circle-left',
'fa-chevron-circle-right',
'fa-chevron-circle-up',
'fa-chevron-circle-down',
'fa-html5',
'fa-css3',
'fa-anchor',
'fa-unlock-alt',
'fa-bullseye',
'fa-ellipsis-h',
'fa-ellipsis-v',
'fa-rss-square',
'fa-play-circle',
'fa-ticket',
'fa-minus-square',
'fa-minus-square-o',
'fa-level-up',
'fa-level-down',
'fa-check-square',
'fa-pencil-square',
'fa-external-link-square',
'fa-share-square',
'fa-compass',
'fa-caret-square-o-down',
'fa-caret-square-o-up',
'fa-caret-square-o-right',
'fa-eur',
'fa-gbp',
'fa-usd',
'fa-inr',
'fa-jpy',
'fa-rub',
'fa-krw',
'fa-btc',
'fa-file',
'fa-file-text',
'fa-sort-alpha-asc',
'fa-sort-alpha-desc',
'fa-sort-amount-asc',
'fa-sort-amount-desc',
'fa-sort-numeric-asc',
'fa-sort-numeric-desc',
'fa-thumbs-up',
'fa-thumbs-down',
'fa-youtube-square',
'fa-youtube',
'fa-xing',
'fa-xing-square',
'fa-youtube-play',
'fa-dropbox',
'fa-stack-overflow',
'fa-instagram',
'fa-flickr',
'fa-adn',
'fa-bitbucket',
'fa-bitbucket-square',
'fa-tumblr',
'fa-tumblr-square',
'fa-long-arrow-down',
'fa-long-arrow-up',
'fa-long-arrow-left',
'fa-long-arrow-right',
'fa-apple',
'fa-windows',
'fa-android',
'fa-linux',
'fa-dribbble',
'fa-skype',
'fa-foursquare',
'fa-trello',
'fa-female',
'fa-male',
'fa-gittip',
'fa-sun-o',
'fa-moon-o',
'fa-archive',
'fa-bug',
'fa-vk',
'fa-weibo',
'fa-renren',
'fa-pagelines',
'fa-stack-exchange',
'fa-arrow-circle-o-right',
'fa-arrow-circle-o-left',
'fa-caret-square-o-left',
'fa-dot-circle-o',
'fa-wheelchair',
'fa-vimeo-square',
'fa-try',
'fa-plus-square-o',
'fa-space-shuttle',
'fa-slack',
'fa-envelope-square',
'fa-wordpress',
'fa-openid',
'fa-institution',
'fa-bank',
'fa-university',
'fa-mortar-board',
'fa-graduation-cap',
'fa-yahoo',
'fa-google',
'fa-reddit',
'fa-reddit-square',
'fa-stumbleupon-circle',
'fa-stumbleupon',
'fa-delicious',
'fa-digg',
'fa-pied-piper-square',
'fa-pied-piper',
'fa-pied-piper-alt',
'fa-pied-piper-pp',
'fa-drupal',
'fa-joomla',
'fa-language',
'fa-fax',
'fa-building',
'fa-child',
'fa-paw',
'fa-spoon',
'fa-cube',
'fa-cubes',
'fa-behance',
'fa-behance-square',
'fa-steam',
'fa-steam-square',
'fa-recycle',
'fa-automobile',
'fa-car',
'fa-cab',
'fa-tree',
'fa-spotify',
'fa-deviantart',
'fa-soundcloud',
'fa-database',
'fa-file-pdf-o',
'fa-file-word-o',
'fa-file-excel-o',
'fa-file-powerpoint-o',
'fa-file-photo-o',
'fa-file-picture-o',
'fa-file-image-o',
'fa-file-zip-o',
'fa-file-archive-o',
'fa-file-sound-o',
'fa-file-movie-o',
'fa-file-code-o',
'fa-vine',
'fa-codepen',
'fa-jsfiddle',
'fa-life-bouy',
'fa-support',
'fa-life-ring',
'fa-circle-o-notch',
'fa-rebel',
'fa-empire',
'fa-git-square',
'fa-git',
'fa-hacker-news',
'fa-tencent-weibo',
'fa-qq',
'fa-wechat',
'fa-send',
'fa-paper-plane',
'fa-send-o',
'fa-paper-plane-o',
'fa-history',
'fa-circle-thin',
'fa-header',
'fa-paragraph',
'fa-sliders',
'fa-share-alt',
'fa-share-alt-square',
'fa-bomb',
'fa-soccer-ball',
'fa-futbol-o',
'fa-tty',
'fa-binoculars',
'fa-plug',
'fa-slideshare',
'fa-twitch',
'fa-yelp',
'fa-newspaper-o',
'fa-wifi',
'fa-calculator',
'fa-paypal',
'fa-google-wallet',
'fa-cc-visa',
'fa-cc-mastercard',
'fa-cc-discover',
'fa-cc-amex',
'fa-cc-paypal',
'fa-cc-stripe',
'fa-bell-slash',
'fa-bell-slash-o',
'fa-trash',
'fa-copyright',
'fa-at',
'fa-eyedropper',
'fa-paint-brush',
'fa-birthday-cake',
'fa-area-chart',
'fa-pie-chart',
'fa-line-chart',
'fa-lastfm',
'fa-lastfm-square',
'fa-toggle-off',
'fa-toggle-on',
'fa-bicycle',
'fa-bus',
'fa-ioxhost',
'fa-angellist',
'fa-cc',
'fa-shekel',
'fa-sheqel',
'fa-ils',
'fa-meanpath',
'fa-buysellads',
'fa-connectdevelop',
'fa-dashcube',
'fa-forumbee',
'fa-leanpub',
'fa-sellsy',
'fa-shirtsinbulk',
'fa-simplybuilt',
'fa-skyatlas',
'fa-cart-plus',
'fa-cart-arrow-down',
'fa-diamond',
'fa-ship',
'fa-user-secret',
'fa-motorcycle',
'fa-street-view',
'fa-heartbeat',
'fa-venus',
'fa-mars',
'fa-mercury',
'fa-transgender',
'fa-transgender-alt',
'fa-venus-double',
'fa-mars-double',
'fa-venus-mars',
'fa-mars-stroke',
'fa-mars-stroke-v',
'fa-mars-stroke-h',
'fa-neuter',
'fa-facebook-official',
'fa-pinterest-p',
'fa-whatsapp',
'fa-server',
'fa-user-plus',
'fa-user-times',
'fa-hotel',
'fa-bed',
'fa-viacoin',
'fa-train',
'fa-subway',
'fa-medium',
'fa-git',
'fa-y-combinator-square',
'fa-yc-square',
'fa-hacker-news',
'fa-yc',
'fa-y-combinator',
'fa-optin-monster',
'fa-opencart',
'fa-expeditedssl',
'fa-battery-4',
'fa-battery-full',
'fa-battery-3',
'fa-battery-three-quarters',
'fa-battery-2',
'fa-battery-half',
'fa-battery-1',
'fa-battery-quarter',
'fa-battery-0',
'fa-battery-empty',
'fa-mouse-pointer',
'fa-i-cursor',
'fa-object-group',
'fa-object-ungroup',
'fa-sticky-note',
'fa-sticky-note-o',
'fa-cc-jcb',
'fa-cc-diners-club',
'fa-clone',
'fa-balance-scale',
'fa-hourglass-o',
'fa-hourglass-1',
'fa-hourglass-start',
'fa-hourglass-2',
'fa-hourglass-half',
'fa-hourglass-3',
'fa-hourglass-end',
'fa-hourglass',
'fa-hand-grab-o',
'fa-hand-rock-o',
'fa-hand-stop-o',
'fa-hand-paper-o',
'fa-hand-scissors-o',
'fa-hand-lizard-o',
'fa-hand-spock-o',
'fa-hand-pointer-o',
'fa-hand-peace-o',
'fa-trademark',
'fa-registered',
'fa-creative-commons',
'fa-gg',
'fa-gg-circle',
'fa-tripadvisor',
'fa-odnoklassniki',
'fa-odnoklassniki-square',
'fa-get-pocket',
'fa-wikipedia-w',
'fa-safari',
'fa-chrome',
'fa-firefox',
'fa-opera',
'fa-internet-explorer',
'fa-tv',
'fa-television',
'fa-contao',
'fa-500px',
'fa-amazon',
'fa-calendar-plus-o',
'fa-calendar-minus-o',
'fa-calendar-times-o',
'fa-calendar-check-o',
'fa-industry',
'fa-map-pin',
'fa-map-signs',
'fa-map-o',
'fa-map',
'fa-commenting',
'fa-commenting-o',
'fa-houzz',
'fa-vimeo',
'fa-black-tie',
'fa-fonticons',
'fa-reddit-alien',
'fa-edge',
'fa-credit-card-alt',
'fa-codiepie:before',
'fa-modx',
'fa-fort-awesome',
'fa-usb',
'fa-product-hunt',
'fa-mixcloud',
'fa-scribd',
'fa-pause-circle',
'fa-pause-circle-o',
'fa-stop-circle',
'fa-stop-circle-o',
'fa-shopping-bag',
'fa-shopping-basket',
'fa-hashtag',
'fa-bluetooth',
'fa-bluetooth-b',
'fa-percent',
'fa-gitlab',
'fa-wpbeginner',
'fa-wpforms',
'fa-envira',
'fa-universal-access',
'fa-wheelchair-alt',
'fa-question-circle-o',
'fa-blind',
'fa-audio-description',
'fa-volume-control-phone',
'fa-braille',
'fa-assistive-listening-systems',
'fa-asl-interpreting',
'fa-american-sign-language-interpreting',
'fa-deafness',
'fa-hard-of-hearing',
'fa-deaf',
'fa-glide',
'fa-glide-g',
'fa-signing',
'fa-sign-language',
'fa-low-vision',
'fa-viadeo',
'fa-viadeo-square',
'fa-snapchat',
'fa-snapchat-ghost',
'fa-snapchat-square',
'fa-first-order',
'fa-yoast',
'fa-themeisle',
'fa-google-plus-circle',
'fa-google-plus-official',
'fa-fa',
'fa-font-awesome',
'fa-handshake-o',
'fa-envelope-open',
'fa-envelope-open-o',
'fa-linode',
'fa-address-book',
'fa-address-book-o',
'fa-vcard',
'fa-address-card',
'fa-vcard-o',
'fa-address-card-o',
'fa-user-circle',
'fa-user-circle-o',
'fa-user-o:before',
'fa-id-badge',
'fa-drivers-license',
'fa-id-card',
'fa-drivers-license-o',
'fa-id-card-o',
'fa-quora',
'fa-free-code-camp',
'fa-telegram',
'fa-thermometer-4',
'fa-thermometer',
'fa-thermometer-full',
'fa-thermometer-3',
'fa-thermometer-three-quarters',
'fa-thermometer-2',
'fa-thermometer-half',
'fa-thermometer-1',
'fa-thermometer-quarter',
'fa-thermometer-0:',
'fa-thermometer-empty',
'fa-shower',
'fa-bathtub',
'fa-s15',
'fa-bath',
'fa-podcast',
'fa-window-maximize',
'fa-window-minimize',
'fa-window-restore',
'fa-times-rectangle',
'fa-window-close',
'fa-times-rectangle-o',
'fa-window-close-o',
'fa-bandcamp',
'fa-grav',
'fa-etsy',
'fa-imdb',
'fa-ravelry',
'fa-eercast',
'fa-microchip',
'fa-snowflake-o',
'fa-superpowers',
'fa-wpexplorer',
'fa-meetup',
);
}
public static function getIconColors() {
return array(
'bluegrey',
'white',
'red',
'orange',
'yellow',
'green',
'blue',
'sky',
'indigo',
'violet',
'pink',
'lightgreytext',
'lightbluetext',
);
}
}
diff --git a/src/view/phui/PHUIObjectItemListView.php b/src/view/phui/PHUIObjectItemListView.php
index 53e86382c..fbc390458 100644
--- a/src/view/phui/PHUIObjectItemListView.php
+++ b/src/view/phui/PHUIObjectItemListView.php
@@ -1,160 +1,184 @@
<?php
final class PHUIObjectItemListView extends AphrontTagView {
private $header;
private $items;
private $pager;
private $noDataString;
private $flush;
private $simple;
private $big;
private $drag;
private $allowEmptyList;
private $itemClass = 'phui-oi-standard';
+ private $tail = array();
public function setAllowEmptyList($allow_empty_list) {
$this->allowEmptyList = $allow_empty_list;
return $this;
}
public function getAllowEmptyList() {
return $this->allowEmptyList;
}
public function setFlush($flush) {
$this->flush = $flush;
return $this;
}
public function setHeader($header) {
$this->header = $header;
return $this;
}
public function setPager($pager) {
$this->pager = $pager;
return $this;
}
public function setSimple($simple) {
$this->simple = $simple;
return $this;
}
public function setBig($big) {
$this->big = $big;
return $this;
}
public function setDrag($drag) {
$this->drag = $drag;
$this->setItemClass('phui-oi-drag');
return $this;
}
public function setNoDataString($no_data_string) {
$this->noDataString = $no_data_string;
return $this;
}
public function addItem(PHUIObjectItemView $item) {
$this->items[] = $item;
return $this;
}
public function setItemClass($item_class) {
$this->itemClass = $item_class;
return $this;
}
protected function getTagName() {
return 'ul';
}
+ public function newTailButton() {
+ $button = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setColor(PHUIButtonView::GREY)
+ ->setIcon('fa-chevron-down')
+ ->setText(pht('View All Results'));
+
+ $this->tail[] = $button;
+
+ return $button;
+ }
+
protected function getTagAttributes() {
$classes = array();
$classes[] = 'phui-oi-list-view';
if ($this->flush) {
$classes[] = 'phui-oi-list-flush';
require_celerity_resource('phui-oi-flush-ui-css');
}
if ($this->simple) {
$classes[] = 'phui-oi-list-simple';
require_celerity_resource('phui-oi-simple-ui-css');
}
if ($this->big) {
$classes[] = 'phui-oi-list-big';
require_celerity_resource('phui-oi-big-ui-css');
}
if ($this->drag) {
$classes[] = 'phui-oi-list-drag';
require_celerity_resource('phui-oi-drag-ui-css');
}
return array(
'class' => $classes,
);
}
protected function getTagContent() {
$viewer = $this->getUser();
require_celerity_resource('phui-oi-list-view-css');
require_celerity_resource('phui-oi-color-css');
$header = null;
if (strlen($this->header)) {
$header = phutil_tag(
'h1',
array(
'class' => 'phui-oi-list-header',
),
$this->header);
}
if ($this->items) {
if ($viewer) {
foreach ($this->items as $item) {
$item->setUser($viewer);
}
}
foreach ($this->items as $item) {
$item->addClass($this->itemClass);
}
$items = $this->items;
} else if ($this->allowEmptyList) {
$items = null;
} else {
$string = nonempty($this->noDataString, pht('No data.'));
$string = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NODATA)
->appendChild($string);
$items = phutil_tag(
'li',
array(
'class' => 'phui-oi-empty',
),
$string);
}
$pager = null;
if ($this->pager) {
$pager = $this->pager;
}
+ $tail = array();
+ foreach ($this->tail as $tail_item) {
+ $tail[] = phutil_tag(
+ 'li',
+ array(
+ 'class' => 'phui-oi-tail',
+ ),
+ $tail_item);
+ }
+
return array(
$header,
$items,
+ $tail,
$pager,
$this->renderChildren(),
);
}
}
diff --git a/src/view/phui/PHUIObjectItemView.php b/src/view/phui/PHUIObjectItemView.php
index e1c67c7f3..463f34a2a 100644
--- a/src/view/phui/PHUIObjectItemView.php
+++ b/src/view/phui/PHUIObjectItemView.php
@@ -1,851 +1,843 @@
<?php
final class PHUIObjectItemView extends AphrontTagView {
private $objectName;
private $header;
private $subhead;
private $href;
private $attributes = array();
private $icons = array();
private $barColor;
private $object;
private $effect;
private $statusIcon;
private $handleIcons = array();
private $bylines = array();
private $grippable;
private $actions = array();
private $headIcons = array();
private $disabled;
private $imageURI;
private $imageHref;
private $imageIcon;
private $titleText;
private $badge;
private $countdownNum;
private $countdownNoun;
private $sideColumn;
private $coverImage;
private $description;
private $clickable;
private $selectableName;
private $selectableValue;
private $isSelected;
private $isForbidden;
public function setDisabled($disabled) {
$this->disabled = $disabled;
return $this;
}
public function getDisabled() {
return $this->disabled;
}
public function addHeadIcon($icon) {
$this->headIcons[] = $icon;
return $this;
}
public function setObjectName($name) {
$this->objectName = $name;
return $this;
}
public function setGrippable($grippable) {
$this->grippable = $grippable;
return $this;
}
public function getGrippable() {
return $this->grippable;
}
public function setEffect($effect) {
$this->effect = $effect;
return $this;
}
public function getEffect() {
return $this->effect;
}
public function setObject($object) {
$this->object = $object;
return $this;
}
public function getObject() {
return $this->object;
}
public function setHref($href) {
$this->href = $href;
return $this;
}
public function getHref() {
return $this->href;
}
public function setHeader($header) {
$this->header = $header;
return $this;
}
public function setSubHead($subhead) {
$this->subhead = $subhead;
return $this;
}
public function setBadge(PHUIBadgeMiniView $badge) {
$this->badge = $badge;
return $this;
}
public function setCountdown($num, $noun) {
$this->countdownNum = $num;
$this->countdownNoun = $noun;
return $this;
}
public function setTitleText($title_text) {
$this->titleText = $title_text;
return $this;
}
public function getTitleText() {
return $this->titleText;
}
public function getHeader() {
return $this->header;
}
public function addByline($byline) {
$this->bylines[] = $byline;
return $this;
}
public function setImageURI($image_uri) {
$this->imageURI = $image_uri;
return $this;
}
public function setImageHref($image_href) {
$this->imageHref = $image_href;
return $this;
}
public function getImageURI() {
return $this->imageURI;
}
public function setImageIcon($image_icon) {
if (!$image_icon instanceof PHUIIconView) {
$image_icon = id(new PHUIIconView())
->setIcon($image_icon);
}
$this->imageIcon = $image_icon;
return $this;
}
public function getImageIcon() {
return $this->imageIcon;
}
public function setCoverImage($image) {
$this->coverImage = $image;
return $this;
}
public function setDescription($description) {
$this->description = $description;
return $this;
}
public function setSelectable(
$name,
$value,
$is_selected,
$is_forbidden = false) {
$this->selectableName = $name;
$this->selectableValue = $value;
$this->isSelected = $is_selected;
$this->isForbidden = $is_forbidden;
return $this;
}
public function setClickable($clickable) {
$this->clickable = $clickable;
return $this;
}
public function getClickable() {
return $this->clickable;
}
public function setEpoch($epoch) {
$date = phabricator_datetime($epoch, $this->getUser());
$this->addIcon('none', $date);
return $this;
}
public function addAction(PHUIListItemView $action) {
if (count($this->actions) >= 3) {
throw new Exception(pht('Limit 3 actions per item.'));
}
$this->actions[] = $action;
return $this;
}
public function addIcon($icon, $label = null, $attributes = array()) {
$this->icons[] = array(
'icon' => $icon,
'label' => $label,
'attributes' => $attributes,
);
return $this;
}
/**
* This method has been deprecated, use @{method:setImageIcon} instead.
*
* @deprecated
*/
public function setIcon($icon) {
phlog(
pht('Deprecated call to setIcon(), use setImageIcon() instead.'));
return $this->setImageIcon($icon);
}
public function setStatusIcon($icon, $label = null) {
$this->statusIcon = array(
'icon' => $icon,
'label' => $label,
);
return $this;
}
public function addHandleIcon(
PhabricatorObjectHandle $handle,
$label = null) {
$this->handleIcons[] = array(
'icon' => $handle,
'label' => $label,
);
return $this;
}
public function setBarColor($bar_color) {
$this->barColor = $bar_color;
return $this;
}
public function getBarColor() {
return $this->barColor;
}
public function addAttribute($attribute) {
if (!empty($attribute)) {
$this->attributes[] = $attribute;
}
return $this;
}
public function setSideColumn($column) {
$this->sideColumn = $column;
return $this;
}
protected function getTagName() {
return 'li';
}
protected function getTagAttributes() {
$sigils = array();
$item_classes = array();
$item_classes[] = 'phui-oi';
if ($this->icons) {
$item_classes[] = 'phui-oi-with-icons';
}
if ($this->attributes) {
$item_classes[] = 'phui-oi-with-attrs';
}
if ($this->handleIcons) {
$item_classes[] = 'phui-oi-with-handle-icons';
}
if ($this->barColor) {
$item_classes[] = 'phui-oi-bar-color-'.$this->barColor;
} else {
$item_classes[] = 'phui-oi-no-bar';
}
if ($this->actions) {
$n = count($this->actions);
$item_classes[] = 'phui-oi-with-actions';
$item_classes[] = 'phui-oi-with-'.$n.'-actions';
}
if ($this->disabled) {
$item_classes[] = 'phui-oi-disabled';
}
switch ($this->effect) {
case 'highlighted':
$item_classes[] = 'phui-oi-highlighted';
break;
case 'selected':
$item_classes[] = 'phui-oi-selected';
break;
case 'visited':
$item_classes[] = 'phui-oi-visited';
break;
case null:
break;
default:
throw new Exception(pht('Invalid effect!'));
}
if ($this->isForbidden) {
$item_classes[] = 'phui-oi-forbidden';
} else if ($this->isSelected) {
$item_classes[] = 'phui-oi-selected';
}
if ($this->selectableName !== null && !$this->isForbidden) {
$item_classes[] = 'phui-oi-selectable';
$sigils[] = 'phui-oi-selectable';
Javelin::initBehavior('phui-selectable-list');
}
if ($this->getGrippable()) {
$item_classes[] = 'phui-oi-grippable';
}
if ($this->getImageURI()) {
$item_classes[] = 'phui-oi-with-image';
}
if ($this->getImageIcon()) {
$item_classes[] = 'phui-oi-with-image-icon';
}
if ($this->getClickable()) {
Javelin::initBehavior('linked-container');
$item_classes[] = 'phui-oi-linked-container';
$sigils[] = 'linked-container';
}
return array(
'class' => $item_classes,
'sigil' => $sigils,
);
}
protected function getTagContent() {
$viewer = $this->getUser();
$content_classes = array();
$content_classes[] = 'phui-oi-content';
$header_name = array();
if ($viewer) {
$header_name[] = id(new PHUISpacesNamespaceContextView())
->setUser($viewer)
->setObject($this->object);
}
if ($this->objectName) {
$header_name[] = array(
phutil_tag(
'span',
array(
'class' => 'phui-oi-objname',
),
$this->objectName),
' ',
);
}
$title_text = null;
if ($this->titleText) {
$title_text = $this->titleText;
} else if ($this->href) {
$title_text = $this->header;
}
$header_link = phutil_tag(
$this->href ? 'a' : 'div',
array(
'href' => $this->href,
'class' => 'phui-oi-link',
'title' => $title_text,
),
$this->header);
$description_tag = null;
if ($this->description) {
$decription_id = celerity_generate_unique_node_id();
$description_tag = id(new PHUITagView())
->setIcon('fa-ellipsis-h')
->addClass('phui-oi-description-tag')
->setType(PHUITagView::TYPE_SHADE)
->setColor(PHUITagView::COLOR_GREY)
->addSigil('jx-toggle-class')
->setSlimShady(true)
->setMetaData(array(
'map' => array(
$decription_id => 'phui-oi-description-reveal',
),
));
}
- // Wrap the header content in a <span> with the "slippery" sigil. This
- // prevents us from beginning a drag if you click the text (like "T123"),
- // but not if you click the white space after the header.
$header = phutil_tag(
'div',
array(
'class' => 'phui-oi-name',
),
- javelin_tag(
- 'span',
- array(
- 'sigil' => 'slippery',
- ),
- array(
- $this->headIcons,
- $header_name,
- $header_link,
- $description_tag,
- )));
+ array(
+ $this->headIcons,
+ $header_name,
+ $header_link,
+ $description_tag,
+ ));
$icons = array();
if ($this->icons) {
$icon_list = array();
foreach ($this->icons as $spec) {
$icon = $spec['icon'];
$icon = id(new PHUIIconView())
->setIcon($icon)
->addClass('phui-oi-icon-image');
if (isset($spec['attributes']['tip'])) {
$sigil = 'has-tooltip';
$meta = array(
'tip' => $spec['attributes']['tip'],
'align' => 'W',
);
$icon->addSigil($sigil);
$icon->setMetadata($meta);
}
$label = phutil_tag(
'span',
array(
'class' => 'phui-oi-icon-label',
),
$spec['label']);
$classes = array();
$classes[] = 'phui-oi-icon';
if (isset($spec['attributes']['class'])) {
$classes[] = $spec['attributes']['class'];
}
$icon_list[] = javelin_tag(
'li',
array(
'class' => implode(' ', $classes),
),
array(
$icon,
$label,
));
}
$icons[] = phutil_tag(
'ul',
array(
'class' => 'phui-oi-icons',
),
$icon_list);
}
$handle_bar = null;
if ($this->handleIcons) {
$handle_bar = array();
foreach ($this->handleIcons as $handleicon) {
$handle_bar[] =
$this->renderHandleIcon($handleicon['icon'], $handleicon['label']);
}
$handle_bar = phutil_tag(
'li',
array(
'class' => 'phui-oi-handle-icons',
),
$handle_bar);
}
$bylines = array();
if ($this->bylines) {
foreach ($this->bylines as $byline) {
$bylines[] = phutil_tag(
'div',
array(
'class' => 'phui-oi-byline',
),
$byline);
}
$bylines = phutil_tag(
'div',
array(
'class' => 'phui-oi-bylines',
),
$bylines);
}
$subhead = null;
if ($this->subhead) {
$subhead = phutil_tag(
'div',
array(
'class' => 'phui-oi-subhead',
),
$this->subhead);
}
if ($this->description) {
$subhead = phutil_tag(
'div',
array(
'class' => 'phui-oi-subhead phui-oi-description',
'id' => $decription_id,
),
$this->description);
}
if ($icons) {
$icons = phutil_tag(
'div',
array(
'class' => 'phui-object-icon-pane',
),
$icons);
}
$attrs = null;
if ($this->attributes || $handle_bar) {
$attrs = array();
$spacer = phutil_tag(
'span',
array(
'class' => 'phui-oi-attribute-spacer',
),
"\xC2\xB7");
$first = true;
foreach ($this->attributes as $attribute) {
$attrs[] = phutil_tag(
'li',
array(
'class' => 'phui-oi-attribute',
),
array(
($first ? null : $spacer),
$attribute,
));
$first = false;
}
$attrs = phutil_tag(
'ul',
array(
'class' => 'phui-oi-attributes',
),
array(
$handle_bar,
$attrs,
));
}
$status = null;
if ($this->statusIcon) {
$icon = $this->statusIcon;
$status = $this->renderStatusIcon($icon['icon'], $icon['label']);
}
$grippable = null;
if ($this->getGrippable()) {
$grippable = phutil_tag(
'div',
array(
'class' => 'phui-oi-grip',
),
'');
}
$content = phutil_tag(
'div',
array(
'class' => implode(' ', $content_classes),
),
array(
$subhead,
$attrs,
$this->renderChildren(),
));
$image = null;
if ($this->getImageURI()) {
$image = phutil_tag(
'div',
array(
'class' => 'phui-oi-image',
'style' => 'background-image: url('.$this->getImageURI().')',
),
'');
} else if ($this->getImageIcon()) {
$image = phutil_tag(
'div',
array(
'class' => 'phui-oi-image-icon',
),
$this->getImageIcon());
}
if ($image && (strlen($this->href) || strlen($this->imageHref))) {
$image_href = ($this->imageHref) ? $this->imageHref : $this->href;
$image = phutil_tag(
'a',
array(
'href' => $image_href,
),
$image);
}
/* Build a fake table */
$column0 = null;
if ($status) {
$column0 = phutil_tag(
'div',
array(
'class' => 'phui-oi-col0',
),
$status);
}
if ($this->badge) {
$column0 = phutil_tag(
'div',
array(
'class' => 'phui-oi-col0 phui-oi-badge',
),
$this->badge);
}
if ($this->countdownNum) {
$countdown = phutil_tag(
'div',
array(
'class' => 'phui-oi-countdown-number',
),
array(
phutil_tag_div('', $this->countdownNum),
phutil_tag_div('', $this->countdownNoun),
));
$column0 = phutil_tag(
'div',
array(
'class' => 'phui-oi-col0 phui-oi-countdown',
),
$countdown);
}
if ($this->selectableName !== null) {
if (!$this->isForbidden) {
$checkbox = phutil_tag(
'input',
array(
'type' => 'checkbox',
'name' => $this->selectableName,
'value' => $this->selectableValue,
'checked' => ($this->isSelected ? 'checked' : null),
));
} else {
$checkbox = null;
}
$column0 = phutil_tag(
'div',
array(
'class' => 'phui-oi-col0 phui-oi-checkbox',
),
$checkbox);
}
$column1 = phutil_tag(
'div',
array(
'class' => 'phui-oi-col1',
),
array(
$header,
$content,
));
$column2 = null;
if ($icons || $bylines) {
$column2 = phutil_tag(
'div',
array(
'class' => 'phui-oi-col2',
),
array(
$icons,
$bylines,
));
}
/* Fixed width, right column container. */
$column3 = null;
if ($this->sideColumn) {
$column3 = phutil_tag(
'div',
array(
'class' => 'phui-oi-col2 phui-oi-side-column',
),
array(
$this->sideColumn,
));
}
$table = phutil_tag(
'div',
array(
'class' => 'phui-oi-table',
),
phutil_tag_div(
'phui-oi-table-row',
array(
$column0,
$column1,
$column2,
$column3,
)));
$box = phutil_tag(
'div',
array(
'class' => 'phui-oi-content-box',
),
array(
$grippable,
$table,
));
$actions = array();
if ($this->actions) {
Javelin::initBehavior('phabricator-tooltips');
foreach (array_reverse($this->actions) as $action) {
$action->setRenderNameAsTooltip(true);
$actions[] = $action;
}
$actions = phutil_tag(
'ul',
array(
'class' => 'phui-oi-actions',
),
$actions);
}
$frame_content = phutil_tag(
'div',
array(
'class' => 'phui-oi-frame-content',
),
array(
$actions,
$image,
$box,
));
$frame_cover = null;
if ($this->coverImage) {
$cover_image = phutil_tag(
'img',
array(
'src' => $this->coverImage,
'class' => 'phui-oi-cover-image',
));
$frame_cover = phutil_tag(
'div',
array(
'class' => 'phui-oi-frame-cover',
),
$cover_image);
}
$frame = phutil_tag(
'div',
array(
'class' => 'phui-oi-frame',
),
array(
$frame_cover,
$frame_content,
));
return $frame;
}
private function renderStatusIcon($icon, $label) {
Javelin::initBehavior('phabricator-tooltips');
$icon = id(new PHUIIconView())
->setIcon($icon);
$options = array(
'class' => 'phui-oi-status-icon',
);
if (strlen($label)) {
$options['sigil'] = 'has-tooltip';
$options['meta'] = array('tip' => $label, 'size' => 300);
}
return javelin_tag('div', $options, $icon);
}
private function renderHandleIcon(PhabricatorObjectHandle $handle, $label) {
Javelin::initBehavior('phabricator-tooltips');
$options = array(
'class' => 'phui-oi-handle-icon',
'style' => 'background-image: url('.$handle->getImageURI().')',
);
if (strlen($label)) {
$options['sigil'] = 'has-tooltip';
$options['meta'] = array('tip' => $label, 'align' => 'E');
}
return javelin_tag('span', $options, '');
}
}
diff --git a/src/view/phui/PHUIPagerView.php b/src/view/phui/PHUIPagerView.php
index b78efcda9..2bb3a8276 100644
--- a/src/view/phui/PHUIPagerView.php
+++ b/src/view/phui/PHUIPagerView.php
@@ -1,234 +1,247 @@
<?php
final class PHUIPagerView extends AphrontView {
private $offset;
private $pageSize = 100;
private $count;
private $hasMorePages;
private $uri;
private $pagingParameter;
private $surroundingPages = 2;
private $enableKeyboardShortcuts;
public function setPageSize($page_size) {
$this->pageSize = max(1, $page_size);
return $this;
}
public function setOffset($offset) {
$this->offset = max(0, $offset);
return $this;
}
public function getOffset() {
return $this->offset;
}
public function getPageSize() {
return $this->pageSize;
}
public function setCount($count) {
$this->count = $count;
return $this;
}
public function setHasMorePages($has_more) {
$this->hasMorePages = $has_more;
return $this;
}
public function setURI(PhutilURI $uri, $paging_parameter) {
$this->uri = $uri;
$this->pagingParameter = $paging_parameter;
return $this;
}
public function readFromRequest(AphrontRequest $request) {
$this->uri = $request->getRequestURI();
$this->pagingParameter = 'offset';
$this->offset = $request->getInt($this->pagingParameter);
return $this;
}
public function willShowPagingControls() {
return $this->hasMorePages || $this->getOffset();
}
public function getHasMorePages() {
return $this->hasMorePages;
}
public function setSurroundingPages($pages) {
$this->surroundingPages = max(0, $pages);
return $this;
}
private function computeCount() {
if ($this->count !== null) {
return $this->count;
}
return $this->getOffset()
+ $this->getPageSize()
+ ($this->hasMorePages ? 1 : 0);
}
private function isExactCountKnown() {
return $this->count !== null;
}
/**
* A common paging strategy is to select one extra record and use that to
* indicate that there's an additional page (this doesn't give you a
* complete page count but is often faster than counting the total number
* of items). This method will take a result array, slice it down to the
* page size if necessary, and call setHasMorePages() if there are more than
* one page of results.
*
* $results = queryfx_all(
* $conn,
* 'SELECT ... LIMIT %d, %d',
* $pager->getOffset(),
* $pager->getPageSize() + 1);
* $results = $pager->sliceResults($results);
*
* @param list Result array.
* @return list One page of results.
*/
public function sliceResults(array $results) {
if (count($results) > $this->getPageSize()) {
$results = array_slice($results, 0, $this->getPageSize(), true);
$this->setHasMorePages(true);
}
return $results;
}
public function setEnableKeyboardShortcuts($enable) {
$this->enableKeyboardShortcuts = $enable;
return $this;
}
public function render() {
if (!$this->uri) {
throw new PhutilInvalidStateException('setURI');
}
require_celerity_resource('phui-pager-css');
$page = (int)floor($this->getOffset() / $this->getPageSize());
$last = ((int)ceil($this->computeCount() / $this->getPageSize())) - 1;
$near = $this->surroundingPages;
$min = $page - $near;
$max = $page + $near;
// Limit the window size to no larger than the number of available pages.
if ($max - $min > $last) {
$max = $min + $last;
if ($max == $min) {
return phutil_tag('div', array('class' => 'phui-pager-view'), '');
}
}
// Slide the window so it is entirely over displayable pages.
if ($min < 0) {
$max += 0 - $min;
$min += 0 - $min;
}
if ($max > $last) {
$min -= $max - $last;
$max -= $max - $last;
}
// Build up a list of <index, label, css-class> tuples which describe the
// links we'll display, then render them all at once.
$links = array();
$prev_index = null;
$next_index = null;
if ($min > 0) {
$links[] = array(0, pht('First'), null);
}
if ($page > 0) {
$links[] = array($page - 1, pht('Prev'), null);
$prev_index = $page - 1;
}
for ($ii = $min; $ii <= $max; $ii++) {
$links[] = array($ii, $ii + 1, ($ii == $page) ? 'current' : null);
}
if ($page < $last && $last > 0) {
$links[] = array($page + 1, pht('Next'), null);
$next_index = $page + 1;
}
if ($max < ($last - 1)) {
$links[] = array($last, pht('Last'), null);
}
$base_uri = $this->uri;
$parameter = $this->pagingParameter;
if ($this->enableKeyboardShortcuts) {
$pager_links = array();
$pager_index = array(
'prev' => $prev_index,
'next' => $next_index,
);
foreach ($pager_index as $key => $index) {
if ($index !== null) {
$display_index = $this->getDisplayIndex($index);
- $pager_links[$key] = (string)$base_uri->alter(
- $parameter,
- $display_index);
+
+ $uri = id(clone $base_uri);
+ if ($display_index === null) {
+ $uri->removeQueryParam($parameter);
+ } else {
+ $uri->replaceQueryParam($parameter, $display_index);
+ }
+
+ $pager_links[$key] = phutil_string_cast($uri);
}
}
Javelin::initBehavior('phabricator-keyboard-pager', $pager_links);
}
// Convert tuples into rendered nodes.
$rendered_links = array();
foreach ($links as $link) {
list($index, $label, $class) = $link;
$display_index = $this->getDisplayIndex($index);
- $link = $base_uri->alter($parameter, $display_index);
+
+ $uri = id(clone $base_uri);
+ if ($display_index === null) {
+ $uri->removeQueryParam($parameter);
+ } else {
+ $uri->replaceQueryParam($parameter, $display_index);
+ }
+
$rendered_links[] = id(new PHUIButtonView())
->setTag('a')
- ->setHref($link)
+ ->setHref($uri)
->setColor(PHUIButtonView::GREY)
->addClass('mml')
->addClass($class)
->setText($label);
}
return phutil_tag(
'div',
array(
'class' => 'phui-pager-view',
),
$rendered_links);
}
private function getDisplayIndex($page_index) {
$page_size = $this->getPageSize();
// Use a 1-based sequence for display so that the number in the URI is
// the same as the page number you're on.
if ($page_index == 0) {
// No need for the first page to say page=1.
$display_index = null;
} else {
$display_index = $page_index * $page_size;
}
return $display_index;
}
}
diff --git a/src/view/phui/PHUITimelineEventView.php b/src/view/phui/PHUITimelineEventView.php
index 86628058f..501361108 100644
--- a/src/view/phui/PHUITimelineEventView.php
+++ b/src/view/phui/PHUITimelineEventView.php
@@ -1,726 +1,746 @@
<?php
final class PHUITimelineEventView extends AphrontView {
const DELIMITER = " \xC2\xB7 ";
private $userHandle;
private $title;
private $icon;
private $color;
private $classes = array();
private $contentSource;
private $dateCreated;
private $anchor;
private $isEditable;
private $isEdited;
private $isRemovable;
private $transactionPHID;
private $isPreview;
private $eventGroup = array();
private $hideByDefault;
private $token;
private $tokenRemoved;
private $quoteTargetID;
private $isNormalComment;
private $quoteRef;
private $reallyMajorEvent;
private $hideCommentOptions = false;
private $authorPHID;
private $badges = array();
private $pinboardItems = array();
private $isSilent;
private $isMFA;
+ private $isLockOverride;
public function setAuthorPHID($author_phid) {
$this->authorPHID = $author_phid;
return $this;
}
public function getAuthorPHID() {
return $this->authorPHID;
}
public function setQuoteRef($quote_ref) {
$this->quoteRef = $quote_ref;
return $this;
}
public function getQuoteRef() {
return $this->quoteRef;
}
public function setQuoteTargetID($quote_target_id) {
$this->quoteTargetID = $quote_target_id;
return $this;
}
public function getQuoteTargetID() {
return $this->quoteTargetID;
}
public function setIsNormalComment($is_normal_comment) {
$this->isNormalComment = $is_normal_comment;
return $this;
}
public function getIsNormalComment() {
return $this->isNormalComment;
}
public function setHideByDefault($hide_by_default) {
$this->hideByDefault = $hide_by_default;
return $this;
}
public function getHideByDefault() {
return $this->hideByDefault;
}
public function setTransactionPHID($transaction_phid) {
$this->transactionPHID = $transaction_phid;
return $this;
}
public function getTransactionPHID() {
return $this->transactionPHID;
}
public function setIsEdited($is_edited) {
$this->isEdited = $is_edited;
return $this;
}
public function getIsEdited() {
return $this->isEdited;
}
public function setIsPreview($is_preview) {
$this->isPreview = $is_preview;
return $this;
}
public function getIsPreview() {
return $this->isPreview;
}
public function setIsEditable($is_editable) {
$this->isEditable = $is_editable;
return $this;
}
public function getIsEditable() {
return $this->isEditable;
}
public function setIsRemovable($is_removable) {
$this->isRemovable = $is_removable;
return $this;
}
public function getIsRemovable() {
return $this->isRemovable;
}
public function setDateCreated($date_created) {
$this->dateCreated = $date_created;
return $this;
}
public function getDateCreated() {
return $this->dateCreated;
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function getContentSource() {
return $this->contentSource;
}
public function setUserHandle(PhabricatorObjectHandle $handle) {
$this->userHandle = $handle;
return $this;
}
public function setAnchor($anchor) {
$this->anchor = $anchor;
return $this;
}
public function getAnchor() {
return $this->anchor;
}
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function addClass($class) {
$this->classes[] = $class;
return $this;
}
public function addBadge(PHUIBadgeMiniView $badge) {
$this->badges[] = $badge;
return $this;
}
public function setIcon($icon) {
$this->icon = $icon;
return $this;
}
public function setColor($color) {
$this->color = $color;
return $this;
}
public function setIsSilent($is_silent) {
$this->isSilent = $is_silent;
return $this;
}
public function getIsSilent() {
return $this->isSilent;
}
public function setIsMFA($is_mfa) {
$this->isMFA = $is_mfa;
return $this;
}
public function getIsMFA() {
return $this->isMFA;
}
+ public function setIsLockOverride($is_override) {
+ $this->isLockOverride = $is_override;
+ return $this;
+ }
+
+ public function getIsLockOverride() {
+ return $this->isLockOverride;
+ }
+
public function setReallyMajorEvent($me) {
$this->reallyMajorEvent = $me;
return $this;
}
public function setHideCommentOptions($hide_comment_options) {
$this->hideCommentOptions = $hide_comment_options;
return $this;
}
public function getHideCommentOptions() {
return $this->hideCommentOptions;
}
public function addPinboardItem(PHUIPinboardItemView $item) {
$this->pinboardItems[] = $item;
return $this;
}
public function setToken($token, $removed = false) {
$this->token = $token;
$this->tokenRemoved = $removed;
return $this;
}
public function getEventGroup() {
return array_merge(array($this), $this->eventGroup);
}
public function addEventToGroup(PHUITimelineEventView $event) {
$this->eventGroup[] = $event;
return $this;
}
protected function shouldRenderEventTitle() {
if ($this->title === null) {
return false;
}
return true;
}
protected function renderEventTitle($force_icon, $has_menu, $extra) {
$title = $this->title;
$title_classes = array();
$title_classes[] = 'phui-timeline-title';
$icon = null;
if ($this->icon || $force_icon) {
$title_classes[] = 'phui-timeline-title-with-icon';
}
if ($has_menu) {
$title_classes[] = 'phui-timeline-title-with-menu';
}
if ($this->icon) {
$fill_classes = array();
$fill_classes[] = 'phui-timeline-icon-fill';
if ($this->color) {
$fill_classes[] = 'fill-has-color';
$fill_classes[] = 'phui-timeline-icon-fill-'.$this->color;
}
$icon = id(new PHUIIconView())
->setIcon($this->icon)
->addClass('phui-timeline-icon');
$icon = phutil_tag(
'span',
array(
'class' => implode(' ', $fill_classes),
),
$icon);
}
$token = null;
if ($this->token) {
$token = id(new PHUIIconView())
->addClass('phui-timeline-token')
->setSpriteSheet(PHUIIconView::SPRITE_TOKENS)
->setSpriteIcon($this->token);
if ($this->tokenRemoved) {
$token->addClass('strikethrough');
}
}
$title = phutil_tag(
'div',
array(
'class' => implode(' ', $title_classes),
),
array($icon, $token, $title, $extra));
return $title;
}
public function render() {
$events = $this->getEventGroup();
// Move events with icons first.
$icon_keys = array();
foreach ($this->getEventGroup() as $key => $event) {
if ($event->icon) {
$icon_keys[] = $key;
}
}
$events = array_select_keys($events, $icon_keys) + $events;
$force_icon = (bool)$icon_keys;
$menu = null;
$items = array();
if (!$this->getIsPreview() && !$this->getHideCommentOptions()) {
foreach ($this->getEventGroup() as $event) {
$items[] = $event->getMenuItems($this->anchor);
}
$items = array_mergev($items);
}
if ($items) {
$icon = id(new PHUIIconView())
->setIcon('fa-caret-down');
$aural = javelin_tag(
'span',
array(
'aural' => true,
),
pht('Comment Actions'));
if ($items) {
$sigil = 'phui-dropdown-menu';
Javelin::initBehavior('phui-dropdown-menu');
} else {
$sigil = null;
}
$action_list = id(new PhabricatorActionListView())
->setUser($this->getUser());
foreach ($items as $item) {
$action_list->addAction($item);
}
$menu = javelin_tag(
$items ? 'a' : 'span',
array(
'href' => '#',
'class' => 'phui-timeline-menu',
'sigil' => $sigil,
'aria-haspopup' => 'true',
'aria-expanded' => 'false',
'meta' => $action_list->getDropdownMenuMetadata(),
),
array(
$aural,
$icon,
));
$has_menu = true;
} else {
$has_menu = false;
}
// Render "extra" information (timestamp, etc).
$extra = $this->renderExtra($events);
$show_badges = false;
$group_titles = array();
$group_items = array();
$group_children = array();
foreach ($events as $event) {
if ($event->shouldRenderEventTitle()) {
// Render the group anchor here, outside the title box. If we render
// it inside the title box it ends up completely hidden and Chrome 55
// refuses to jump to it. See T11997 for discussion.
if ($extra && $this->anchor) {
$group_titles[] = id(new PhabricatorAnchorView())
->setAnchorName($this->anchor)
->render();
}
$group_titles[] = $event->renderEventTitle(
$force_icon,
$has_menu,
$extra);
// Don't render this information more than once.
$extra = null;
}
if ($event->hasChildren()) {
$group_children[] = $event->renderChildren();
$show_badges = true;
}
}
$image_uri = $this->userHandle->getImageURI();
$wedge = phutil_tag(
'div',
array(
'class' => 'phui-timeline-wedge',
'style' => (nonempty($image_uri)) ? '' : 'display: none;',
),
'');
$image = null;
$badges = null;
if ($image_uri) {
- $image = phutil_tag(
+ $image = javelin_tag(
($this->userHandle->getURI()) ? 'a' : 'div',
array(
'style' => 'background-image: url('.$image_uri.')',
- 'class' => 'phui-timeline-image visual-only',
+ 'class' => 'phui-timeline-image',
'href' => $this->userHandle->getURI(),
+ 'aural' => false,
),
'');
if ($this->badges && $show_badges) {
$flex = new PHUIBadgeBoxView();
$flex->addItems($this->badges);
$flex->setCollapsed(true);
$badges = phutil_tag(
'div',
array(
'class' => 'phui-timeline-badges',
),
$flex);
}
}
$content_classes = array();
$content_classes[] = 'phui-timeline-content';
$classes = array();
$classes[] = 'phui-timeline-event-view';
if ($group_children) {
$classes[] = 'phui-timeline-major-event';
$content = phutil_tag(
'div',
array(
'class' => 'phui-timeline-inner-content',
),
array(
$group_titles,
$menu,
phutil_tag(
'div',
array(
'class' => 'phui-timeline-core-content',
),
$group_children),
));
} else {
$classes[] = 'phui-timeline-minor-event';
$content = $group_titles;
}
$content = phutil_tag(
'div',
array(
'class' => 'phui-timeline-group',
),
$content);
// Image Events
$pinboard = null;
if ($this->pinboardItems) {
$pinboard = new PHUIPinboardView();
foreach ($this->pinboardItems as $item) {
$pinboard->addItem($item);
}
}
$content = phutil_tag(
'div',
array(
'class' => implode(' ', $content_classes),
),
array($image, $badges, $wedge, $content, $pinboard));
$outer_classes = $this->classes;
$outer_classes[] = 'phui-timeline-shell';
$color = null;
foreach ($this->getEventGroup() as $event) {
if ($event->color) {
$color = $event->color;
break;
}
}
if ($color) {
$outer_classes[] = 'phui-timeline-'.$color;
}
$sigil = null;
$meta = null;
if ($this->getTransactionPHID()) {
$sigil = 'transaction';
$meta = array(
'phid' => $this->getTransactionPHID(),
'anchor' => $this->anchor,
);
}
$major_event = null;
if ($this->reallyMajorEvent) {
$major_event = phutil_tag(
'div',
array(
'class' => 'phui-timeline-event-view '.
'phui-timeline-spacer '.
'phui-timeline-spacer-bold',
'',
));
}
return array(
javelin_tag(
'div',
array(
'class' => implode(' ', $outer_classes),
'id' => $this->anchor ? 'anchor-'.$this->anchor : null,
'sigil' => $sigil,
'meta' => $meta,
),
phutil_tag(
'div',
array(
'class' => implode(' ', $classes),
),
$content)),
$major_event,
);
}
private function renderExtra(array $events) {
$extra = array();
if ($this->getIsPreview()) {
$extra[] = pht('PREVIEW');
} else {
foreach ($events as $event) {
if ($event->getIsEdited()) {
$extra[] = pht('Edited');
break;
}
}
$source = $this->getContentSource();
$content_source = null;
if ($source) {
$content_source = id(new PhabricatorContentSourceView())
->setContentSource($source)
->setUser($this->getUser());
$content_source = pht('Via %s', $content_source->getSourceName());
}
$date_created = null;
foreach ($events as $event) {
if ($event->getDateCreated()) {
if ($date_created === null) {
$date_created = $event->getDateCreated();
} else {
$date_created = min($event->getDateCreated(), $date_created);
}
}
}
if ($date_created) {
$date = phabricator_datetime(
$date_created,
$this->getUser());
if ($this->anchor) {
Javelin::initBehavior('phabricator-watch-anchor');
Javelin::initBehavior('phabricator-tooltips');
$date = array(
javelin_tag(
'a',
array(
'href' => '#'.$this->anchor,
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $content_source,
),
),
$date),
);
}
$extra[] = $date;
}
// If this edit was applied silently, give user a hint that they should
// not expect to have received any mail or notifications.
if ($this->getIsSilent()) {
$extra[] = id(new PHUIIconView())
- ->setIcon('fa-bell-slash', 'red')
+ ->setIcon('fa-bell-slash', 'white')
+ ->setEmblemColor('red')
->setTooltip(pht('Silent Edit'));
}
// If this edit was applied while the actor was in high-security mode,
// provide a hint that it was extra authentic.
if ($this->getIsMFA()) {
$extra[] = id(new PHUIIconView())
- ->setIcon('fa-vcard', 'pink')
+ ->setIcon('fa-vcard', 'white')
+ ->setEmblemColor('pink')
->setTooltip(pht('MFA Authenticated'));
}
+
+ if ($this->getIsLockOverride()) {
+ $extra[] = id(new PHUIIconView())
+ ->setIcon('fa-chain-broken', 'white')
+ ->setEmblemColor('violet')
+ ->setTooltip(pht('Lock Overridden'));
+ }
}
$extra = javelin_tag(
'span',
array(
'class' => 'phui-timeline-extra',
),
phutil_implode_html(
javelin_tag(
'span',
array(
'aural' => false,
),
self::DELIMITER),
$extra));
return $extra;
}
private function getMenuItems($anchor) {
$xaction_phid = $this->getTransactionPHID();
$items = array();
if ($this->getIsEditable()) {
$items[] = id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setHref('/transactions/edit/'.$xaction_phid.'/')
->setName(pht('Edit Comment'))
->addSigil('transaction-edit')
->setMetadata(
array(
'anchor' => $anchor,
));
}
if ($this->getQuoteTargetID()) {
$ref = null;
if ($this->getQuoteRef()) {
$ref = $this->getQuoteRef();
if ($anchor) {
$ref = $ref.'#'.$anchor;
}
}
$items[] = id(new PhabricatorActionView())
->setIcon('fa-quote-left')
->setName(pht('Quote Comment'))
->setHref('#')
->addSigil('transaction-quote')
->setMetadata(
array(
'targetID' => $this->getQuoteTargetID(),
'uri' => '/transactions/quote/'.$xaction_phid.'/',
'ref' => $ref,
));
}
if ($this->getIsNormalComment()) {
$items[] = id(new PhabricatorActionView())
->setIcon('fa-code')
->setHref('/transactions/raw/'.$xaction_phid.'/')
->setName(pht('View Remarkup'))
->addSigil('transaction-raw')
->setMetadata(
array(
'anchor' => $anchor,
));
$content_source = $this->getContentSource();
$source_email = PhabricatorEmailContentSource::SOURCECONST;
if ($content_source->getSource() == $source_email) {
$source_id = $content_source->getContentSourceParameter('id');
if ($source_id) {
$items[] = id(new PhabricatorActionView())
->setIcon('fa-envelope-o')
->setHref('/transactions/raw/'.$xaction_phid.'/?email')
->setName(pht('View Email Body'))
->addSigil('transaction-raw')
->setMetadata(
array(
'anchor' => $anchor,
));
}
}
}
if ($this->getIsEdited()) {
$items[] = id(new PhabricatorActionView())
->setIcon('fa-list')
->setHref('/transactions/history/'.$xaction_phid.'/')
->setName(pht('View Edit History'))
->setWorkflow(true);
}
if ($this->getIsRemovable()) {
$items[] = id(new PhabricatorActionView())
->setType(PhabricatorActionView::TYPE_DIVIDER);
$items[] = id(new PhabricatorActionView())
->setIcon('fa-trash-o')
->setHref('/transactions/remove/'.$xaction_phid.'/')
->setName(pht('Remove Comment'))
->setColor(PhabricatorActionView::RED)
->addSigil('transaction-remove')
->setMetadata(
array(
'anchor' => $anchor,
));
}
return $items;
}
}
diff --git a/src/view/phui/PHUITimelineView.php b/src/view/phui/PHUITimelineView.php
index d0e942f46..2e6d8298c 100644
--- a/src/view/phui/PHUITimelineView.php
+++ b/src/view/phui/PHUITimelineView.php
@@ -1,283 +1,296 @@
<?php
final class PHUITimelineView extends AphrontView {
private $events = array();
private $id;
private $shouldTerminate = false;
private $shouldAddSpacers = true;
private $pager;
private $viewData = array();
private $quoteTargetID;
private $quoteRef;
public function setID($id) {
$this->id = $id;
return $this;
}
public function setShouldTerminate($term) {
$this->shouldTerminate = $term;
return $this;
}
public function setShouldAddSpacers($bool) {
$this->shouldAddSpacers = $bool;
return $this;
}
public function setPager(AphrontCursorPagerView $pager) {
$this->pager = $pager;
return $this;
}
public function getPager() {
return $this->pager;
}
public function addEvent(PHUITimelineEventView $event) {
$this->events[] = $event;
return $this;
}
public function setViewData(array $data) {
$this->viewData = $data;
return $this;
}
public function getViewData() {
return $this->viewData;
}
public function setQuoteTargetID($quote_target_id) {
$this->quoteTargetID = $quote_target_id;
return $this;
}
public function getQuoteTargetID() {
return $this->quoteTargetID;
}
public function setQuoteRef($quote_ref) {
$this->quoteRef = $quote_ref;
return $this;
}
public function getQuoteRef() {
return $this->quoteRef;
}
public function render() {
if ($this->getPager()) {
if ($this->id === null) {
$this->id = celerity_generate_unique_node_id();
}
Javelin::initBehavior(
'phabricator-show-older-transactions',
array(
'timelineID' => $this->id,
'viewData' => $this->getViewData(),
));
}
$events = $this->buildEvents();
return phutil_tag(
'div',
array(
'class' => 'phui-timeline-view',
'id' => $this->id,
),
array(
phutil_tag(
'h3',
array(
'class' => 'aural-only',
),
pht('Event Timeline')),
$events,
));
}
public function buildEvents() {
require_celerity_resource('phui-timeline-view-css');
$spacer = self::renderSpacer();
// Track why we're hiding older results.
$hide_reason = null;
$hide = array();
$show = array();
// Bucket timeline events into events we'll hide by default (because they
// predate your most recent interaction with the object) and events we'll
// show by default.
foreach ($this->events as $event) {
if ($event->getHideByDefault()) {
$hide[] = $event;
} else {
$show[] = $event;
}
}
// If you've never interacted with the object, all the events will be shown
// by default. We may still need to paginate if there are a large number
// of events.
$more = (bool)$hide;
if ($more) {
$hide_reason = 'comment';
}
if ($this->getPager()) {
if ($this->getPager()->getHasMoreResults()) {
if (!$more) {
$hide_reason = 'limit';
}
$more = true;
}
}
$events = array();
if ($more && $this->getPager()) {
switch ($hide_reason) {
case 'comment':
$hide_help = pht(
'Changes from before your most recent comment are hidden.');
break;
case 'limit':
default:
$hide_help = pht(
'There are a very large number of changes, so older changes are '.
'hidden.');
break;
}
$uri = $this->getPager()->getNextPageURI();
- $uri->setQueryParam('quoteTargetID', $this->getQuoteTargetID());
- $uri->setQueryParam('quoteRef', $this->getQuoteRef());
+
+ $target_id = $this->getQuoteTargetID();
+ if ($target_id === null) {
+ $uri->removeQueryParam('quoteTargetID');
+ } else {
+ $uri->replaceQueryParam('quoteTargetID', $target_id);
+ }
+
+ $quote_ref = $this->getQuoteRef();
+ if ($quote_ref === null) {
+ $uri->removeQueryParam('quoteRef');
+ } else {
+ $uri->replaceQueryParam('quoteRef', $quote_ref);
+ }
+
$events[] = javelin_tag(
'div',
array(
'sigil' => 'show-older-block',
'class' => 'phui-timeline-older-transactions-are-hidden',
),
array(
$hide_help,
' ',
javelin_tag(
'a',
array(
'href' => (string)$uri,
'mustcapture' => true,
'sigil' => 'show-older-link',
),
pht('Show Older Changes')),
));
if ($show) {
$events[] = $spacer;
}
}
if ($show) {
$this->prepareBadgeData($show);
$events[] = phutil_implode_html($spacer, $show);
}
if ($events) {
if ($this->shouldAddSpacers) {
$events = array($spacer, $events, $spacer);
}
} else {
$events = array($spacer);
}
if ($this->shouldTerminate) {
$events[] = self::renderEnder();
}
return $events;
}
public static function renderSpacer() {
return phutil_tag(
'div',
array(
'class' => 'phui-timeline-event-view '.
'phui-timeline-spacer',
),
'');
}
public static function renderEnder() {
return phutil_tag(
'div',
array(
'class' => 'phui-timeline-event-view '.
'the-worlds-end',
),
'');
}
private function prepareBadgeData(array $events) {
assert_instances_of($events, 'PHUITimelineEventView');
$viewer = $this->getUser();
$can_use_badges = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorBadgesApplication',
$viewer);
if (!$can_use_badges) {
return;
}
$user_phid_type = PhabricatorPeopleUserPHIDType::TYPECONST;
$user_phids = array();
foreach ($events as $key => $event) {
$author_phid = $event->getAuthorPHID();
if (!$author_phid) {
unset($events[$key]);
continue;
}
if (phid_get_type($author_phid) != $user_phid_type) {
// This is likely an application actor, like "Herald" or "Harbormaster".
// They can't have badges.
unset($events[$key]);
continue;
}
$user_phids[$author_phid] = $author_phid;
}
if (!$user_phids) {
return;
}
$users = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs($user_phids)
->needBadgeAwards(true)
->execute();
$users = mpull($users, null, 'getPHID');
foreach ($events as $event) {
$user_phid = $event->getAuthorPHID();
if (!array_key_exists($user_phid, $users)) {
continue;
}
$badges = $users[$user_phid]->getRecentBadgeAwards();
foreach ($badges as $badge) {
$badge_view = id(new PHUIBadgeMiniView())
->setIcon($badge['icon'])
->setQuality($badge['quality'])
->setHeader($badge['name'])
->setTipDirection('E')
->setHref('/badges/view/'.$badge['id'].'/');
$event->addBadge($badge_view);
}
}
}
}
diff --git a/src/view/widget/AphrontStackTraceView.php b/src/view/widget/AphrontStackTraceView.php
index 1d0616df3..edb805af8 100644
--- a/src/view/widget/AphrontStackTraceView.php
+++ b/src/view/widget/AphrontStackTraceView.php
@@ -1,108 +1,107 @@
<?php
final class AphrontStackTraceView extends AphrontView {
private $trace;
public function setTrace($trace) {
$this->trace = $trace;
return $this;
}
public function render() {
- $user = $this->getUser();
$trace = $this->trace;
$libraries = PhutilBootloader::getInstance()->getAllLibraries();
// TODO: Make this configurable?
$path = 'https://secure.phabricator.com/diffusion/%s/browse/master/src/';
$callsigns = array(
'arcanist' => 'ARC',
'phutil' => 'PHU',
'phabricator' => 'P',
);
$rows = array();
$depth = count($trace);
foreach ($trace as $part) {
$lib = null;
$file = idx($part, 'file');
$relative = $file;
foreach ($libraries as $library) {
$root = phutil_get_library_root($library);
if (Filesystem::isDescendant($file, $root)) {
$lib = $library;
$relative = Filesystem::readablePath($file, $root);
break;
}
}
$where = '';
if (isset($part['class'])) {
$where .= $part['class'].'::';
}
if (isset($part['function'])) {
$where .= $part['function'].'()';
}
if ($file) {
if (isset($callsigns[$lib])) {
$attrs = array('title' => $file);
if (empty($attrs['href'])) {
$attrs['href'] = sprintf($path, $callsigns[$lib]).
str_replace(DIRECTORY_SEPARATOR, '/', $relative).
'$'.$part['line'];
$attrs['target'] = '_blank';
}
$file_name = phutil_tag(
'a',
$attrs,
$relative);
} else {
$file_name = phutil_tag(
'span',
array(
'title' => $file,
),
$relative);
}
$file_name = hsprintf('%s : %d', $file_name, $part['line']);
} else {
$file_name = phutil_tag('em', array(), '(Internal)');
}
$rows[] = array(
$depth--,
$lib,
$file_name,
$where,
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
pht('Depth'),
pht('Library'),
pht('File'),
pht('Where'),
));
$table->setColumnClasses(
array(
'n',
'',
'',
'wide',
));
return phutil_tag(
'div',
array(
'class' => 'exception-trace',
),
$table->render());
}
}
diff --git a/support/lint/browser.jshintrc b/support/lint/browser.jshintrc
index b88c931ee..2a9c65bdd 100644
--- a/support/lint/browser.jshintrc
+++ b/support/lint/browser.jshintrc
@@ -1,25 +1,25 @@
{
"bitwise": true,
"curly": true,
"freeze": true,
"immed": true,
"indent": 2,
- "latedef": true,
+ "latedef": "nofunc",
"newcap": true,
"noarg": true,
"quotmark": "single",
"undef": true,
- "unused": true,
+ "unused": "vars",
"expr": true,
"loopfunc": true,
"sub": true,
"globals": {
"JX": false,
"d3": false,
"__DEV__": false
},
"browser": true
}
diff --git a/support/startup/PhabricatorStartup.php b/support/startup/PhabricatorStartup.php
index 1bfb74d88..4c577ca20 100644
--- a/support/startup/PhabricatorStartup.php
+++ b/support/startup/PhabricatorStartup.php
@@ -1,828 +1,828 @@
<?php
/**
* Handle request startup, before loading the environment or libraries. This
* class bootstraps the request state up to the point where we can enter
* Phabricator code.
*
* NOTE: This class MUST NOT have any dependencies. It runs before libraries
* load.
*
* Rate Limiting
* =============
*
* Phabricator limits the rate at which clients can request pages, and issues
* HTTP 429 "Too Many Requests" responses if clients request too many pages too
* quickly. Although this is not a complete defense against high-volume attacks,
* it can protect an install against aggressive crawlers, security scanners,
* and some types of malicious activity.
*
* To perform rate limiting, each page increments a score counter for the
* requesting user's IP. The page can give the IP more points for an expensive
* request, or fewer for an authetnicated request.
*
* Score counters are kept in buckets, and writes move to a new bucket every
* minute. After a few minutes (defined by @{method:getRateLimitBucketCount}),
* the oldest bucket is discarded. This provides a simple mechanism for keeping
* track of scores without needing to store, access, or read very much data.
*
* Users are allowed to accumulate up to 1000 points per minute, averaged across
* all of the tracked buckets.
*
* @task info Accessing Request Information
* @task hook Startup Hooks
* @task apocalypse In Case Of Apocalypse
* @task validation Validation
* @task ratelimit Rate Limiting
* @task phases Startup Phase Timers
*/
final class PhabricatorStartup {
private static $startTime;
private static $debugTimeLimit;
private static $accessLog;
private static $capturingOutput;
private static $rawInput;
private static $oldMemoryLimit;
private static $phases;
private static $limits = array();
/* -( Accessing Request Information )-------------------------------------- */
/**
* @task info
*/
public static function getStartTime() {
return self::$startTime;
}
/**
* @task info
*/
public static function getMicrosecondsSinceStart() {
// This is the same as "phutil_microseconds_since()", but we may not have
// loaded libphutil yet.
return (int)(1000000 * (microtime(true) - self::getStartTime()));
}
/**
* @task info
*/
public static function setAccessLog($access_log) {
self::$accessLog = $access_log;
}
/**
* @task info
*/
public static function getRawInput() {
if (self::$rawInput === null) {
$stream = new AphrontRequestStream();
if (isset($_SERVER['HTTP_CONTENT_ENCODING'])) {
$encoding = trim($_SERVER['HTTP_CONTENT_ENCODING']);
$stream->setEncoding($encoding);
}
$input = '';
do {
$bytes = $stream->readData();
if ($bytes === null) {
break;
}
$input .= $bytes;
} while (true);
self::$rawInput = $input;
}
return self::$rawInput;
}
/* -( Startup Hooks )------------------------------------------------------ */
/**
* @param float Request start time, from `microtime(true)`.
* @task hook
*/
public static function didStartup($start_time) {
self::$startTime = $start_time;
self::$phases = array();
self::$accessLog = null;
static $registered;
if (!$registered) {
// NOTE: This protects us against multiple calls to didStartup() in the
// same request, but also against repeated requests to the same
// interpreter state, which we may implement in the future.
register_shutdown_function(array(__CLASS__, 'didShutdown'));
$registered = true;
}
self::setupPHP();
self::verifyPHP();
// If we've made it this far, the environment isn't completely broken so
// we can switch over to relying on our own exception recovery mechanisms.
ini_set('display_errors', 0);
self::connectRateLimits();
self::normalizeInput();
self::verifyRewriteRules();
self::detectPostMaxSizeTriggered();
self::beginOutputCapture();
}
/**
* @task hook
*/
public static function didShutdown() {
// Disconnect any active rate limits before we shut down. If we don't do
// this, requests which exit early will lock a slot in any active
// connection limits, and won't count for rate limits.
self::disconnectRateLimits(array());
$event = error_get_last();
if (!$event) {
return;
}
switch ($event['type']) {
case E_ERROR:
case E_PARSE:
case E_COMPILE_ERROR:
break;
default:
return;
}
$msg = ">>> UNRECOVERABLE FATAL ERROR <<<\n\n";
if ($event) {
// Even though we should be emitting this as text-plain, escape things
// just to be sure since we can't really be sure what the program state
// is when we get here.
$msg .= htmlspecialchars(
$event['message']."\n\n".$event['file'].':'.$event['line'],
ENT_QUOTES,
'UTF-8');
}
// flip dem tables
$msg .= "\n\n\n";
$msg .= "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb\x20\xef\xb8\xb5\x20\xc2\xaf".
"\x5c\x5f\x28\xe3\x83\x84\x29\x5f\x2f\xc2\xaf\x20\xef\xb8\xb5\x20".
"\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb";
self::didFatal($msg);
}
public static function loadCoreLibraries() {
$phabricator_root = dirname(dirname(dirname(__FILE__)));
$libraries_root = dirname($phabricator_root);
$root = null;
if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) {
$root = $_SERVER['PHUTIL_LIBRARY_ROOT'];
}
ini_set(
'include_path',
$libraries_root.PATH_SEPARATOR.ini_get('include_path'));
@include_once $root.'libphutil/src/__phutil_library_init__.php';
if (!@constant('__LIBPHUTIL__')) {
self::didFatal(
"Unable to load libphutil. Put libphutil/ next to phabricator/, or ".
"update your PHP 'include_path' to include the parent directory of ".
"libphutil/.");
}
phutil_load_library('arcanist/src');
// Load Phabricator itself using the absolute path, so we never end up doing
// anything surprising (loading index.php and libraries from different
// directories).
phutil_load_library($phabricator_root.'/src');
}
/* -( Output Capture )----------------------------------------------------- */
public static function beginOutputCapture() {
if (self::$capturingOutput) {
self::didFatal('Already capturing output!');
}
self::$capturingOutput = true;
ob_start();
}
public static function endOutputCapture() {
if (!self::$capturingOutput) {
return null;
}
self::$capturingOutput = false;
return ob_get_clean();
}
/* -( Debug Time Limit )--------------------------------------------------- */
/**
* Set a time limit (in seconds) for the current script. After time expires,
* the script fatals.
*
* This works like `max_execution_time`, but prints out a useful stack trace
* when the time limit expires. This is primarily intended to make it easier
* to debug pages which hang by allowing extraction of a stack trace: set a
* short debug limit, then use the trace to figure out what's happening.
*
* The limit is implemented with a tick function, so enabling it implies
* some accounting overhead.
*
* @param int Time limit in seconds.
* @return void
*/
public static function setDebugTimeLimit($limit) {
self::$debugTimeLimit = $limit;
static $initialized;
if (!$initialized) {
declare(ticks=1);
register_tick_function(array(__CLASS__, 'onDebugTick'));
}
}
/**
* Callback tick function used by @{method:setDebugTimeLimit}.
*
* Fatals with a useful stack trace after the time limit expires.
*
* @return void
*/
public static function onDebugTick() {
$limit = self::$debugTimeLimit;
if (!$limit) {
return;
}
$elapsed = (microtime(true) - self::getStartTime());
if ($elapsed > $limit) {
$frames = array();
foreach (debug_backtrace() as $frame) {
$file = isset($frame['file']) ? $frame['file'] : '-';
$file = basename($file);
$line = isset($frame['line']) ? $frame['line'] : '-';
$class = isset($frame['class']) ? $frame['class'].'->' : null;
$func = isset($frame['function']) ? $frame['function'].'()' : '?';
$frames[] = "{$file}:{$line} {$class}{$func}";
}
self::didFatal(
"Request aborted by debug time limit after {$limit} seconds.\n\n".
"STACK TRACE\n".
implode("\n", $frames));
}
}
/* -( In Case of Apocalypse )---------------------------------------------- */
/**
* Fatal the request completely in response to an exception, sending a plain
* text message to the client. Calls @{method:didFatal} internally.
*
* @param string Brief description of the exception context, like
* `"Rendering Exception"`.
- * @param Exception The exception itself.
+ * @param Throwable The exception itself.
* @param bool True if it's okay to show the exception's stack trace
* to the user. The trace will always be logged.
* @return exit This method **does not return**.
*
* @task apocalypse
*/
public static function didEncounterFatalException(
$note,
- Exception $ex,
+ $ex,
$show_trace) {
$message = '['.$note.'/'.get_class($ex).'] '.$ex->getMessage();
$full_message = $message;
$full_message .= "\n\n";
$full_message .= $ex->getTraceAsString();
if ($show_trace) {
$message = $full_message;
}
self::didFatal($message, $full_message);
}
/**
* Fatal the request completely, sending a plain text message to the client.
*
* @param string Plain text message to send to the client.
* @param string Plain text message to send to the error log. If not
* provided, the client message is used. You can pass a more
* detailed message here (e.g., with stack traces) to avoid
* showing it to users.
* @return exit This method **does not return**.
*
* @task apocalypse
*/
public static function didFatal($message, $log_message = null) {
if ($log_message === null) {
$log_message = $message;
}
self::endOutputCapture();
$access_log = self::$accessLog;
if ($access_log) {
// We may end up here before the access log is initialized, e.g. from
// verifyPHP().
$access_log->setData(
array(
'c' => 500,
));
$access_log->write();
}
header(
'Content-Type: text/plain; charset=utf-8',
$replace = true,
$http_error = 500);
error_log($log_message);
echo $message."\n";
exit(1);
}
/* -( Validation )--------------------------------------------------------- */
/**
* @task validation
*/
private static function setupPHP() {
error_reporting(E_ALL | E_STRICT);
self::$oldMemoryLimit = ini_get('memory_limit');
ini_set('memory_limit', -1);
// If we have libxml, disable the incredibly dangerous entity loader.
if (function_exists('libxml_disable_entity_loader')) {
libxml_disable_entity_loader(true);
}
// See T13060. If the locale for this process (the parent process) is not
// a UTF-8 locale we can encounter problems when launching subprocesses
// which receive UTF-8 parameters in their command line argument list.
@setlocale(LC_ALL, 'en_US.UTF-8');
}
/**
* @task validation
*/
public static function getOldMemoryLimit() {
return self::$oldMemoryLimit;
}
/**
* @task validation
*/
private static function normalizeInput() {
// Replace superglobals with unfiltered versions, disrespect php.ini (we
// filter ourselves).
// NOTE: We don't filter INPUT_SERVER because we don't want to overwrite
// changes made in "preamble.php".
// NOTE: WE don't filter INPUT_POST because we may be constructing it
// lazily if "enable_post_data_reading" is disabled.
$filter = array(
INPUT_GET,
INPUT_ENV,
INPUT_COOKIE,
);
foreach ($filter as $type) {
$filtered = filter_input_array($type, FILTER_UNSAFE_RAW);
if (!is_array($filtered)) {
continue;
}
switch ($type) {
case INPUT_GET:
$_GET = array_merge($_GET, $filtered);
break;
case INPUT_COOKIE:
$_COOKIE = array_merge($_COOKIE, $filtered);
break;
case INPUT_ENV;
$env = array_merge($_ENV, $filtered);
$_ENV = self::filterEnvSuperglobal($env);
break;
}
}
self::rebuildRequest();
}
/**
* @task validation
*/
public static function rebuildRequest() {
// Rebuild $_REQUEST, respecting order declared in ".ini" files.
$order = ini_get('request_order');
if (!$order) {
$order = ini_get('variables_order');
}
if (!$order) {
// $_REQUEST will be empty, so leave it alone.
return;
}
$_REQUEST = array();
for ($ii = 0; $ii < strlen($order); $ii++) {
switch ($order[$ii]) {
case 'G':
$_REQUEST = array_merge($_REQUEST, $_GET);
break;
case 'P':
$_REQUEST = array_merge($_REQUEST, $_POST);
break;
case 'C':
$_REQUEST = array_merge($_REQUEST, $_COOKIE);
break;
default:
// $_ENV and $_SERVER never go into $_REQUEST.
break;
}
}
}
/**
* Adjust `$_ENV` before execution.
*
* Adjustments here primarily impact the environment as seen by subprocesses.
* The environment is forwarded explicitly by @{class:ExecFuture}.
*
* @param map<string, wild> Input `$_ENV`.
* @return map<string, string> Suitable `$_ENV`.
* @task validation
*/
private static function filterEnvSuperglobal(array $env) {
// In some configurations, we may get "argc" and "argv" set in $_ENV.
// These are not real environmental variables, and "argv" may have an array
// value which can not be forwarded to subprocesses. Remove these from the
// environment if they are present.
unset($env['argc']);
unset($env['argv']);
return $env;
}
/**
* @task validation
*/
private static function verifyPHP() {
$required_version = '5.2.3';
if (version_compare(PHP_VERSION, $required_version) < 0) {
self::didFatal(
"You are running PHP version '".PHP_VERSION."', which is older than ".
"the minimum version, '{$required_version}'. Update to at least ".
"'{$required_version}'.");
}
if (get_magic_quotes_gpc()) {
self::didFatal(
"Your server is configured with PHP 'magic_quotes_gpc' enabled. This ".
"feature is 'highly discouraged' by PHP's developers and you must ".
"disable it to run Phabricator. Consult the PHP manual for ".
"instructions.");
}
if (extension_loaded('apc')) {
$apc_version = phpversion('apc');
$known_bad = array(
'3.1.14' => true,
'3.1.15' => true,
'3.1.15-dev' => true,
);
if (isset($known_bad[$apc_version])) {
self::didFatal(
"You have APC {$apc_version} installed. This version of APC is ".
"known to be bad, and does not work with Phabricator (it will ".
"cause Phabricator to fatal unrecoverably with nonsense errors). ".
"Downgrade to version 3.1.13.");
}
}
if (isset($_SERVER['HTTP_PROXY'])) {
self::didFatal(
'This HTTP request included a "Proxy:" header, poisoning the '.
'environment (CVE-2016-5385 / httpoxy). Declining to process this '.
'request. For details, see: https://phurl.io/u/httpoxy');
}
}
/**
* @task validation
*/
private static function verifyRewriteRules() {
if (isset($_REQUEST['__path__']) && strlen($_REQUEST['__path__'])) {
return;
}
if (php_sapi_name() == 'cli-server') {
// Compatibility with PHP 5.4+ built-in web server.
$url = parse_url($_SERVER['REQUEST_URI']);
$_REQUEST['__path__'] = $url['path'];
return;
}
if (!isset($_REQUEST['__path__'])) {
self::didFatal(
"Request parameter '__path__' is not set. Your rewrite rules ".
"are not configured correctly.");
}
if (!strlen($_REQUEST['__path__'])) {
self::didFatal(
"Request parameter '__path__' is set, but empty. Your rewrite rules ".
"are not configured correctly. The '__path__' should always ".
"begin with a '/'.");
}
}
/**
* Detect if this request has had its POST data stripped by exceeding the
* 'post_max_size' PHP configuration limit.
*
* PHP has a setting called 'post_max_size'. If a POST request arrives with
* a body larger than the limit, PHP doesn't generate $_POST but processes
* the request anyway, and provides no formal way to detect that this
* happened.
*
* We can still read the entire body out of `php://input`. However according
* to the documentation the stream isn't available for "multipart/form-data"
* (on nginx + php-fpm it appears that it is available, though, at least) so
* any attempt to generate $_POST would be fragile.
*
* @task validation
*/
private static function detectPostMaxSizeTriggered() {
// If this wasn't a POST, we're fine.
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
return;
}
// If "enable_post_data_reading" is off, we won't have $_POST and this
// condition is effectively impossible.
if (!ini_get('enable_post_data_reading')) {
return;
}
// If there's POST data, clearly we're in good shape.
if ($_POST) {
return;
}
// For HTML5 drag-and-drop file uploads, Safari submits the data as
// "application/x-www-form-urlencoded". For most files this generates
// something in POST because most files decode to some nonempty (albeit
// meaningless) value. However, some files (particularly small images)
// don't decode to anything. If we know this is a drag-and-drop upload,
// we can skip this check.
if (isset($_REQUEST['__upload__'])) {
return;
}
// PHP generates $_POST only for two content types. This routing happens
// in `main/php_content_types.c` in PHP. Normally, all forms use one of
// these content types, but some requests may not -- for example, Firefox
// submits files sent over HTML5 XMLHTTPRequest APIs with the Content-Type
// of the file itself. If we don't have a recognized content type, we
// don't need $_POST.
//
// NOTE: We use strncmp() because the actual content type may be something
// like "multipart/form-data; boundary=...".
//
// NOTE: Chrome sometimes omits this header, see some discussion in T1762
// and http://code.google.com/p/chromium/issues/detail?id=6800
$content_type = isset($_SERVER['CONTENT_TYPE'])
? $_SERVER['CONTENT_TYPE']
: '';
$parsed_types = array(
'application/x-www-form-urlencoded',
'multipart/form-data',
);
$is_parsed_type = false;
foreach ($parsed_types as $parsed_type) {
if (strncmp($content_type, $parsed_type, strlen($parsed_type)) === 0) {
$is_parsed_type = true;
break;
}
}
if (!$is_parsed_type) {
return;
}
// Check for 'Content-Length'. If there's no data, we don't expect $_POST
// to exist.
$length = (int)$_SERVER['CONTENT_LENGTH'];
if (!$length) {
return;
}
// Time to fatal: we know this was a POST with data that should have been
// populated into $_POST, but it wasn't.
$config = ini_get('post_max_size');
self::didFatal(
"As received by the server, this request had a nonzero content length ".
"but no POST data.\n\n".
"Normally, this indicates that it exceeds the 'post_max_size' setting ".
"in the PHP configuration on the server. Increase the 'post_max_size' ".
"setting or reduce the size of the request.\n\n".
"Request size according to 'Content-Length' was '{$length}', ".
"'post_max_size' is set to '{$config}'.");
}
/* -( Rate Limiting )------------------------------------------------------ */
/**
* Add a new client limits.
*
* @param PhabricatorClientLimit New limit.
* @return PhabricatorClientLimit The limit.
*/
public static function addRateLimit(PhabricatorClientLimit $limit) {
self::$limits[] = $limit;
return $limit;
}
/**
* Apply configured rate limits.
*
* If any limit is exceeded, this method terminates the request.
*
* @return void
* @task ratelimit
*/
private static function connectRateLimits() {
$limits = self::$limits;
$reason = null;
$connected = array();
foreach ($limits as $limit) {
$reason = $limit->didConnect();
$connected[] = $limit;
if ($reason !== null) {
break;
}
}
// If we're killing the request here, disconnect any limits that we
// connected to try to keep the accounting straight.
if ($reason !== null) {
foreach ($connected as $limit) {
$limit->didDisconnect(array());
}
self::didRateLimit($reason);
}
}
/**
* Tear down rate limiting and allow limits to score the request.
*
* @param map<string, wild> Additional, freeform request state.
* @return void
* @task ratelimit
*/
public static function disconnectRateLimits(array $request_state) {
$limits = self::$limits;
// Remove all limits before disconnecting them so this works properly if
// it runs twice. (We run this automatically as a shutdown handler.)
self::$limits = array();
foreach ($limits as $limit) {
$limit->didDisconnect($request_state);
}
}
/**
* Emit an HTTP 429 "Too Many Requests" response (indicating that the user
* has exceeded application rate limits) and exit.
*
* @return exit This method **does not return**.
* @task ratelimit
*/
private static function didRateLimit($reason) {
header(
'Content-Type: text/plain; charset=utf-8',
$replace = true,
$http_error = 429);
echo $reason;
exit(1);
}
/* -( Startup Timers )----------------------------------------------------- */
/**
* Record the beginning of a new startup phase.
*
* For phases which occur before @{class:PhabricatorStartup} loads, save the
* time and record it with @{method:recordStartupPhase} after the class is
* available.
*
* @param string Phase name.
* @task phases
*/
public static function beginStartupPhase($phase) {
self::recordStartupPhase($phase, microtime(true));
}
/**
* Record the start time of a previously executed startup phase.
*
* For startup phases which occur after @{class:PhabricatorStartup} loads,
* use @{method:beginStartupPhase} instead. This method can be used to
* record a time before the class loads, then hand it over once the class
* becomes available.
*
* @param string Phase name.
* @param float Phase start time, from `microtime(true)`.
* @task phases
*/
public static function recordStartupPhase($phase, $time) {
self::$phases[$phase] = $time;
}
/**
* Get information about startup phase timings.
*
* Sometimes, performance problems can occur before we start the profiler.
* Since the profiler can't examine these phases, it isn't useful in
* understanding their performance costs.
*
* Instead, the startup process marks when it enters various phases using
* @{method:beginStartupPhase}. A later call to this method can retrieve this
* information, which can be examined to gain greater insight into where
* time was spent. The output is still crude, but better than nothing.
*
* @task phases
*/
public static function getPhases() {
return self::$phases;
}
}
diff --git a/webroot/index.php b/webroot/index.php
index 5c7d79bfa..0014edfa2 100644
--- a/webroot/index.php
+++ b/webroot/index.php
@@ -1,60 +1,102 @@
<?php
phabricator_startup();
+$fatal_exception = null;
try {
PhabricatorStartup::beginStartupPhase('libraries');
PhabricatorStartup::loadCoreLibraries();
PhabricatorStartup::beginStartupPhase('purge');
PhabricatorCaches::destroyRequestCache();
PhabricatorStartup::beginStartupPhase('sink');
$sink = new AphrontPHPHTTPSink();
+ // PHP introduced a "Throwable" interface in PHP 7 and began making more
+ // runtime errors throw as "Throwable" errors. This is generally good, but
+ // makes top-level exception handling that is compatible with both PHP 5
+ // and PHP 7 a bit tricky.
+
+ // In PHP 5, "Throwable" does not exist, so "catch (Throwable $ex)" catches
+ // nothing.
+
+ // In PHP 7, various runtime conditions raise an Error which is a Throwable
+ // but NOT an Exception, so "catch (Exception $ex)" will not catch them.
+
+ // To cover both cases, we "catch (Exception $ex)" to catch everything in
+ // PHP 5, and most things in PHP 7. Then, we "catch (Throwable $ex)" to catch
+ // everything else in PHP 7. For the most part, we only need to do this at
+ // the top level.
+
+ $main_exception = null;
try {
PhabricatorStartup::beginStartupPhase('run');
AphrontApplicationConfiguration::runHTTPRequest($sink);
} catch (Exception $ex) {
+ $main_exception = $ex;
+ } catch (Throwable $ex) {
+ $main_exception = $ex;
+ }
+
+ if ($main_exception) {
+ $response_exception = null;
try {
$response = new AphrontUnhandledExceptionResponse();
- $response->setException($ex);
+ $response->setException($main_exception);
+ $response->setShowStackTraces($sink->getShowStackTraces());
PhabricatorStartup::endOutputCapture();
$sink->writeResponse($response);
- } catch (Exception $response_exception) {
- // If we hit a rendering exception, ignore it and throw the original
- // exception. It is generally more interesting and more likely to be
- // the root cause.
- throw $ex;
+ } catch (Exception $ex) {
+ $response_exception = $ex;
+ } catch (Throwable $ex) {
+ $response_exception = $ex;
+ }
+
+ // If we hit a rendering exception, ignore it and throw the original
+ // exception. It is generally more interesting and more likely to be
+ // the root cause.
+
+ if ($response_exception) {
+ throw $main_exception;
}
}
} catch (Exception $ex) {
- PhabricatorStartup::didEncounterFatalException('Core Exception', $ex, false);
+ $fatal_exception = $ex;
+} catch (Throwable $ex) {
+ $fatal_exception = $ex;
+}
+
+if ($fatal_exception) {
+ PhabricatorStartup::didEncounterFatalException(
+ 'Core Exception',
+ $fatal_exception,
+ false);
}
function phabricator_startup() {
// Load the PhabricatorStartup class itself.
$t_startup = microtime(true);
$root = dirname(dirname(__FILE__));
require_once $root.'/support/startup/PhabricatorStartup.php';
// Load client limit classes so the preamble can configure limits.
require_once $root.'/support/startup/PhabricatorClientLimit.php';
require_once $root.'/support/startup/PhabricatorClientRateLimit.php';
require_once $root.'/support/startup/PhabricatorClientConnectionLimit.php';
// If the preamble script exists, load it.
$t_preamble = microtime(true);
$preamble_path = $root.'/support/preamble.php';
if (file_exists($preamble_path)) {
require_once $preamble_path;
}
$t_hook = microtime(true);
PhabricatorStartup::didStartup($t_startup);
PhabricatorStartup::recordStartupPhase('startup.init', $t_startup);
PhabricatorStartup::recordStartupPhase('preamble', $t_preamble);
PhabricatorStartup::recordStartupPhase('hook', $t_hook);
}
diff --git a/webroot/rsrc/css/aphront/table-view.css b/webroot/rsrc/css/aphront/table-view.css
index 1a7d2eb21..fd1a91814 100644
--- a/webroot/rsrc/css/aphront/table-view.css
+++ b/webroot/rsrc/css/aphront/table-view.css
@@ -1,329 +1,367 @@
/**
* @provides aphront-table-view-css
*/
.aphront-table-wrap {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.aphront-table-view {
width: 100%;
border-collapse: collapse;
background: {$page.content};
border: 1px solid {$lightblueborder};
border-bottom: 1px solid {$blueborder};
}
.aphront-table-view-fixed {
table-layout: fixed;
}
.aphront-table-view-fixed th {
box-sizing: border-box;
}
.aphront-table-notice {
padding: 12px 16px;
color: {$darkbluetext};
border-bottom: 1px solid {$thinblueborder};
}
.phui-two-column-view .aphront-table-notice .phui-info-view {
margin: 0;
}
.aphront-table-view tr.alt {
background: {$lightgreybackground};
}
.device-desktop .aphront-table-view tr:hover {
background: {$bluebackground};
}
.device-desktop .aphront-table-view tr.no-data:hover {
background: inherit;
}
-.aphront-table-view th {
+.aphront-table-view th,
+.aphront-table-view td.header {
font-weight: bold;
white-space: nowrap;
color: {$bluetext};
- text-shadow: 0 1px 0 white;
font-weight: bold;
- border-bottom: 1px solid {$thinblueborder};
+ text-shadow: 0 1px 0 white;
background-color: {$lightbluebackground};
}
+.aphront-table-view th {
+ border-bottom: 1px solid {$thinblueborder};
+}
+
th.aphront-table-view-sortable-selected {
background-color: {$greybackground};
}
.aphront-table-view th a,
.aphront-table-view th a:hover,
.aphront-table-view th a:link {
color: {$bluetext};
text-shadow: 0 1px 0 white;
display: block;
text-decoration: none;
}
.aphront-table-view th a:hover {
text-decoration: underline;
color: {$darkbluetext};
}
.aphront-table-view td.header {
- padding: 4px 8px;
- white-space: nowrap;
text-align: right;
- color: {$bluetext};
- font-weight: bold;
- vertical-align: top;
+ border-right: 1px solid {$thinblueborder};
}
.aphront-table-view td {
white-space: nowrap;
vertical-align: middle;
color: {$darkbluetext};
}
.aphront-table-down-sort {
display: inline-block;
margin-top: 5px;
width: 0;
height: 0;
vertical-align: top;
border-top: 4px solid {$bluetext};
border-right: 4px solid transparent;
border-left: 4px solid transparent;
content: "";
}
.aphront-table-up-sort {
display: inline-block;
margin-top: 5px;
width: 0;
height: 0;
vertical-align: top;
border-bottom: 4px solid {$bluetext};
border-right: 4px solid transparent;
border-left: 4px solid transparent;
content: "";
}
/* - Padding -------------------------------------------------------------------
On desktops, we have more horizontal space and use it to space columns out.
On devices, we make each row slightly taller to create a larger hit target
for links.
*/
.aphront-table-view th {
padding: 8px 10px;
}
.aphront-table-view td {
padding: 8px 10px;
}
.device-tablet .aphront-table-view th,
.device-phone .aphront-table-view th {
overflow: hidden;
}
.aphront-table-view td.sorted-column {
background: {$lightbluebackground};
}
.aphront-table-view tr.alt td.sorted-column {
background: {$greybackground};
}
.aphront-table-view td.action {
padding-top: 1px;
padding-bottom: 1px;
}
.aphront-table-view td.larger {
font-size: {$biggerfontsize};
}
.aphront-table-view td.pri {
font-weight: bold;
color: {$darkbluetext};
}
.aphront-table-view td.top {
vertical-align: top;
}
.aphront-table-view td.wide {
white-space: normal;
width: 100%;
}
.aphront-table-view th.right,
.aphront-table-view td.right {
text-align: right;
}
.aphront-table-view td.mono {
font-family: "Monaco", monospace;
font-size: {$smallestfontsize};
}
.aphront-table-view td.n {
font-family: "Monaco", monospace;
font-size: {$smallestfontsize};
text-align: right;
}
.aphront-table-view td.nudgeright, .aphront-table-view th.nudgeright {
padding-right: 0;
}
.aphront-table-view td.wrap {
white-space: normal;
}
.aphront-table-view td.prewrap {
font-family: "Monaco", monospace;
font-size: {$smallestfontsize};
white-space: pre-wrap;
}
.aphront-table-view td.narrow {
width: 1px;
}
.aphront-table-view td.icon, .aphront-table-view th.icon {
width: 1px;
padding: 0px;
}
.aphront-table-view td.icon + td.icon {
padding-left: 8px;
}
div.single-display-line-bounds {
width: 100%;
position: relative;
overflow: hidden;
}
span.single-display-line-content {
white-space: pre;
position: absolute;
}
.device-phone span.single-display-line-content {
white-space: nowrap;
position: static;
}
.aphront-table-view td.object-link {
white-space: nowrap;
word-wrap: break-word;
overflow: hidden;
text-overflow: ellipsis;
- max-width: 0;
+ min-width: 320px;
+ max-width: 320px;
}
.aphront-table-view tr.closed td.object-link .object-name,
.aphront-table-view tr.alt-closed td.object-link .object-name {
text-decoration: line-through;
color: rgba({$alphablack}, 0.5);
}
.aphront-table-view tr.closed td.object-link a,
.aphront-table-view tr.alt-closed td.object-link a {
color: rgba({$alphablack}, 0.5);
}
.aphront-table-view tr.closed td.graph-status,
.aphront-table-view tr.alt-closed td.graph-status,
.object-graph-table em {
color: {$lightgreytext};
}
.aphront-table-view tr.highlighted {
background: #fdf9e4;
}
.aphront-table-view tr.alt-highlighted {
background: {$sh-yellowbackground};
}
.aphront-table-view tr.diff-removed,
.aphront-table-view tr.alt-diff-removed {
background: {$lightred}
}
.aphront-table-view tr.diff-added,
.aphront-table-view tr.alt-diff-added {
background: {$lightgreen}
}
.aphront-table-view tr.no-data td {
padding: 16px;
text-align: center;
color: {$lightgreytext};
font-style: italic;
}
.aphront-table-view td.thumb img {
max-width: 64px;
max-height: 64px;
}
.aphront-table-view td.threads {
font-family: monospace;
white-space: pre;
padding: 0 0 0 8px;
}
.aphront-table-view td.threads canvas {
display: block;
}
.aphront-table-view td.radio {
text-align: center;
padding: 2px 4px 0px;
}
.aphront-table-view th.center,
.aphront-table-view td.center {
text-align: center;
}
.device .aphront-table-view td + td.center,
.device .aphront-table-view th + th.center {
padding-left: 3px;
padding-right: 3px;
}
.device-desktop .aphront-table-view-device {
display: none;
}
.device-tablet .aphront-table-view-nodevice,
.device-phone .aphront-table-view-nodevice {
display: none;
}
.aphront-table-view td.link {
padding: 0;
}
.aphront-table-view td.link a {
display: block;
padding: 6px 8px;
font-weight: bold;
}
.phui-object-box .aphront-table-view {
border: none;
}
+
+.object-graph-header {
+ padding: 8px 12px;
+ overflow: hidden;
+ background: {$lightyellow};
+ border-bottom: 1px solid {$lightblueborder};
+ vertical-align: middle;
+}
+
+.object-graph-header .object-graph-header-icon {
+ float: left;
+ margin-top: 10px;
+}
+
+.object-graph-header a.button {
+ float: right;
+}
+
+.object-graph-header-message {
+ margin: 8px 200px 8px 20px;
+}
+
+.device .object-graph-header .object-graph-header-icon {
+ display: none;
+}
+
+.device .object-graph-header-message {
+ clear: both;
+ margin: 0;
+}
+
+.device .object-graph-header a.button {
+ margin: 0 auto 12px;
+ display: block;
+ width: 180px;
+ float: none;
+}
diff --git a/webroot/rsrc/css/application/base/notification-menu.css b/webroot/rsrc/css/application/base/notification-menu.css
index 8db243689..588679860 100644
--- a/webroot/rsrc/css/application/base/notification-menu.css
+++ b/webroot/rsrc/css/application/base/notification-menu.css
@@ -1,164 +1,170 @@
/**
* @provides phabricator-notification-menu-css
*/
.phabricator-notification-menu {
background: {$page.content};
font-size: {$smallerfontsize};
line-height: 18px;
word-wrap: break-word;
overflow-y: auto;
box-shadow: {$dropshadow};
border: 1px solid {$lightgreyborder};
border-radius: 3px;
}
.phabricator-notification {
padding: 8px 12px;
+ color: {$darkgreytext};
}
.phabricator-notification-menu-loading {
text-align: center;
padding: 10px 0;
color: {$lightgreytext};
}
.device-desktop .phabricator-notification-menu,
.device-tablet .phabricator-notification-menu {
position: absolute;
width: 360px;
top: 42px;
}
.device-phone .phabricator-notification-menu {
border-bottom: 1px solid {$thinblueborder};
width: 94%;
max-width: 390px;
top: 42px !important;
left: 3% !important;
position: absolute;
}
.phabricator-notification-list.pm {
padding: 0;
}
.phabricator-notification-list .phabricator-notification {
padding: 8px;
}
.phabricator-notification-menu .phabricator-notification {
cursor: pointer;
}
.device-desktop .phabricator-notification-menu .phabricator-notification:hover {
background: {$lightgreybackground};
}
.device-desktop .phabricator-notification-menu
.phabricator-notification-unread.phabricator-notification:hover {
background: {$hoverselectedblue};
}
.phabricator-notification + .phabricator-notification {
border-top: 1px solid {$thinblueborder};
}
.no-notifications {
color: {$lightgreytext};
}
.phabricator-notification-warning {
background: {$sh-yellowbackground};
}
.phabricator-notification-list .phabricator-notification-unread,
.phabricator-notification-menu .phabricator-notification-unread {
background: {$hoverblue};
}
.phabricator-notification-read {
color: {$lightgreytext};
}
.phabricator-notification-foot {
color: {$lightgreytext};
font-size: {$smallestfontsize};
line-height: 18px;
position: relative;
}
.phabricator-notification-unread .phabricator-notification-foot {
padding-left: 10px;
}
.phabricator-notification-foot .phabricator-notification-status {
display: none;
}
.phabricator-notification-unread .phabricator-notification-foot
.phabricator-notification-status {
font-size: 7px;
color: {$lightbluetext};
position: absolute;
display: inline-block;
top: 6px;
left: 0;
}
.phabricator-notification-header {
font-weight: bold;
padding: 10px 12px;
font-size: {$smallerfontsize};
border-bottom: 1px solid {$thinblueborder};
}
.phabricator-notification-header a {
- color: {$darkgreytext};
+ color: {$anchor};
}
.phabricator-notification-header a:hover {
text-decoration: underline;
}
.phabricator-notification-header .phabricator-notification-clear-all {
color: {$anchor};
float: right;
font-weight: normal;
}
.phabricator-notification-footer {
background: {$greybackground};
border-top: 1px solid {$thinblueborder};
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
padding: 8px;
font-size: {$smallerfontsize};
color: {$darkgreytext};
}
.phabricator-notification-footer a {
color: {$darkgreytext};
}
.phabricator-notification-footer a:hover {
text-decoration: underline;
}
.phabricator-notification-menu .aphlict-connection-status {
color: {$lightgreytext};
}
.aphlict-connection-status {
position: relative;
}
.aphlict-connection-status .phui-icon-view {
font-size: 9px;
position: absolute;
top: 4px;
}
.aphlict-connection-status .connection-status-text {
margin-left: 12px;
}
+
+.phabricator-notification .phui-timeline-value {
+ font-style: italic;
+ color: #000;
+}
diff --git a/webroot/rsrc/css/application/config/unhandled-exception.css b/webroot/rsrc/css/application/config/unhandled-exception.css
index 831148cda..cd8ad313d 100644
--- a/webroot/rsrc/css/application/config/unhandled-exception.css
+++ b/webroot/rsrc/css/application/config/unhandled-exception.css
@@ -1,25 +1,53 @@
/**
* @provides unhandled-exception-css
*/
.unhandled-exception-detail {
max-width: 760px;
margin: 24px auto;
background: #fff;
border: 1px solid #c0392b;
border-radius: 3px;
- padding: 0 8px;
+ padding: 8px;
}
.unhandled-exception-detail .unhandled-exception-title {
color: #c0392b;
- padding: 12px 8px;
+ padding: 4px 8px 12px;
border-bottom: 1px solid #f4dddb;
font-size: 16px;
font-weight: 500;
margin: 0;
}
.unhandled-exception-detail .unhandled-exception-body {
padding: 16px 12px;
}
+
+.unhandled-exception-with-stack {
+ max-width: 95%;
+}
+
+.unhandled-exception-stack {
+ background: #fcfcfc;
+ overflow-x: auto;
+ overflow-y: hidden;
+}
+
+.unhandled-exception-stack table {
+ border-spacing: 0;
+ border-collapse: collapse;
+ width: 100%;
+ border: 1px solid #d7d7d7;
+}
+
+.unhandled-exception-stack th {
+ background: #e7e7e7;
+ border-bottom: 1px solid #d7d7d7;
+ padding: 8px;
+}
+
+.unhandled-exception-stack td {
+ padding: 4px 8px;
+ white-space: nowrap;
+}
diff --git a/webroot/rsrc/css/application/differential/changeset-view.css b/webroot/rsrc/css/application/differential/changeset-view.css
index b9683dc7c..233ac4cca 100644
--- a/webroot/rsrc/css/application/differential/changeset-view.css
+++ b/webroot/rsrc/css/application/differential/changeset-view.css
@@ -1,409 +1,483 @@
/**
* @provides differential-changeset-view-css
* @requires phui-inline-comment-view-css
*/
.differential-changeset {
position: relative;
margin: 0;
padding-top: 16px;
overflow-x: auto;
/* Fixes what seems to be a layout bug in Firefox which causes scrollbars,
to appear unpredictably, see discussion in T7690. */
overflow-y: hidden;
}
.device-phone .differential-changeset {
overflow-x: scroll;
-webkit-overflow-scrolling: touch;
}
.differential-diff {
background: {$diff.background};
width: 100%;
border-top: 1px solid {$lightblueborder};
border-bottom: 1px solid {$lightblueborder};
table-layout: fixed;
}
.differential-diff.diff-2up {
min-width: 780px;
}
.differential-diff col.num {
width: 45px;
}
.device .differential-diff.diff-1up col.num {
width: 32px;
}
.differential-diff.diff-2up col.left,
.differential-diff.diff-2up col.right {
width: 49.25%;
}
.differential-diff.diff-1up col.unified {
width: 99.5%;
}
.differential-diff col.copy {
width: 0.5%;
}
.differential-diff col.cov {
width: 1%;
}
.differential-diff td {
vertical-align: top;
white-space: pre-wrap;
word-wrap: break-word;
padding: 1px 8px;
}
.device .differential-diff td {
padding: 1px 4px;
}
-.differential-diff td .zwsp {
- position: absolute;
- width: 0;
-}
-
-.differential-diff th {
- text-align: right;
- padding: 1px 6px 1px 0;
- vertical-align: top;
- background: {$lightbluebackground};
- color: {$bluetext};
- cursor: pointer;
- border-right: 1px solid {$thinblueborder};
- overflow: hidden;
-
- -moz-user-select: -moz-none;
- -khtml-user-select: none;
- -webkit-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
-
.prose-diff {
padding: 12px 0;
white-space: pre-wrap;
color: {$greytext};
}
.prose-diff-frame {
padding: 12px;
}
.prose-diff span.old,
.prose-diff span.new {
padding: 0 2px;
}
.prose-diff span.old,
.prose-diff span.new {
color: {$darkgreytext};
}
-.differential-changeset-immutable .differential-diff th {
+.differential-changeset-immutable .differential-diff td {
cursor: auto;
}
.differential-diff td.old {
background: {$old-background};
}
.differential-diff td.new {
background: {$new-background};
}
.differential-diff td.old-rebase {
background: #ffeeee;
}
.differential-diff td.new-rebase {
background: #eeffee;
}
.differential-diff td.old span.bright,
.differential-diff td.old-full,
.prose-diff span.old {
background: {$old-bright};
}
+
.differential-diff td.new span.bright,
.differential-diff td.new-full,
.prose-diff span.new {
background: {$new-bright};
}
+.differential-diff td span.depth-out,
+.differential-diff td span.depth-in {
+ padding: 2px 0;
+ background-size: 12px 12px;
+ background-repeat: no-repeat;
+ background-position: left center;
+ position: relative;
+ left: -8px;
+ opacity: 0.5;
+}
+
+.differential-diff td span.depth-out {
+ background-image: url(/rsrc/image/chevron-out.png);
+ background-color: {$old-bright};
+}
+
+.differential-diff td span.depth-in {
+ background-position: 1px center;
+ background-image: url(/rsrc/image/chevron-in.png);
+ background-color: {$new-bright};
+}
+
+
.differential-diff td.copy {
min-width: 0.5%;
width: 0.5%;
padding: 0;
+ background: {$lightbluebackground};
}
.differential-diff td.new-copy,
.differential-diff td.new-copy span.bright {
background: {$copy-background};
}
.differential-diff td.new-move,
.differential-diff td.new-move span.bright {
background: {$move-background};
}
.differential-diff td.comment {
background: #dddddd;
}
+.differential-diff .inline > td {
+ padding: 0;
+}
+
+/* Specify line number behaviors after other behaviors because line numbers
+should always have a boring grey background. */
+
+.differential-diff td.n {
+ text-align: right;
+ padding: 1px 6px 1px 0;
+ vertical-align: top;
+ background: {$lightbluebackground};
+ color: {$bluetext};
+ cursor: pointer;
+ border-right: 1px solid {$thinblueborder};
+ overflow: hidden;
+}
+
+.differential-diff td + td.n {
+ border-left: 1px solid {$thinblueborder};
+}
+
+.differential-diff td.n::before {
+ content: attr(data-n);
+}
+
+.differential-diff td.show-context-line.n {
+ cursor: auto;
+}
+
.differential-diff td.cov {
padding: 0;
}
td.cov-U {
background: #dd8866;
}
td.cov-C {
background: #66bbff;
}
td.cov-N {
background: #ddeeff;
}
td.cov-X {
background: #aa00aa;
}
td.cov-I {
background: {$lightgreybackground};
}
.differential-diff td.source-cov-C,
.differential-diff td.source-cov-C span.bright {
background: #cceeff;
}
.differential-diff td.source-cov-U,
.differential-diff td.source-cov-U span.bright {
background: #ffbb99;
}
.differential-diff td.source-cov-N,
.differential-diff td.source-cov-N span.bright {
background: #f3f6ff;
}
.differential-diff td.show-more,
-.differential-diff th.show-context-line,
+.differential-diff td.show-context-line,
.differential-diff td.show-context,
.differential-diff td.differential-shield {
background: {$lightbluebackground};
padding: 12px 0;
border-top: 1px solid {$thinblueborder};
border-bottom: 1px solid {$thinblueborder};
}
.device .differential-diff td.show-more,
-.device .differential-diff th.show-context-line,
+.device .differential-diff td.show-context-line,
.device .differential-diff td.show-context,
.device .differential-diff td.differential-shield {
padding: 6px 0;
}
.differential-diff td.show-more,
.differential-diff td.differential-shield {
font: {$basefont};
font-size: {$smallerfontsize};
white-space: normal;
}
.differential-diff td.show-more {
text-align: center;
color: {$bluetext};
}
-.differential-diff th.show-context-line {
+.differential-diff td.show-context-line {
padding-right: 6px;
}
+.differential-diff td.show-context-line.left-context {
+ border-right: none;
+}
+
.differential-diff td.show-context {
padding-left: 14px;
}
.differential-diff td.differential-shield {
text-align: center;
}
.differential-diff td.differential-shield a {
font-weight: bold;
}
.differential-diff .differential-image-diff {
background-image: url(/rsrc/image/checker_light.png);
}
.differential-diff .differential-image-diff:hover {
background-image: url(/rsrc/image/checker_dark.png);
}
.differential-diff .differential-image-diff td {
padding: 8px;
}
.differential-image-stage {
overflow: auto;
}
.differential-meta-notice {
border-top: 1px solid {$gentle.highlight.border};
border-bottom: 1px solid {$gentle.highlight.border};
background-color: {$gentle.highlight};
padding: 12px;
}
.differential-meta-notice + .differential-diff {
border-top: none;
}
.differential-changeset h1 {
font-size: {$biggestfontsize};
padding: 2px 0 20px 12px;
line-height: 20px;
color: {$blacktext};
}
.device-phone .differential-changeset h1 {
word-break: break-word;
margin-right: 8px;
}
.differential-reticle {
background-color: {$sh-yellowbackground};
border: 1px solid {$sh-yellowborder};
position: absolute;
opacity: 0.5;
top: 0;
left: 0;
box-sizing: border-box;
pointer-events: none;
}
-.differential-diff .inline > td {
- padding: 0;
-}
-
.differential-loading {
border-top: 1px solid {$gentle.highlight.border};
border-bottom: 1px solid {$gentle.highlight.border};
background-color: {$gentle.highlight};
padding: 12px;
text-align: center;
}
.differential-collapse-undo {
color: {$darkbluetext};
padding: 12px;
border: 1px solid {$blue};
text-align: center;
background-color: {$lightblue};
margin: 8px;
}
.differential-collapse-undo a {
font-weight: bold;
}
.differential-file-icon-header .phui-icon-view {
display: inline-block;
margin: 0 6px 2px 0;
vertical-align: middle;
font-size: 14px;
}
.device-phone .differential-file-icon-header .phui-icon-view {
display: none;
}
.differential-changeset-buttons {
float: right;
margin-right: 12px;
}
.device-phone .differential-changeset-buttons .button .phui-button-text {
visibility: hidden;
width: 0;
margin-left: 8px;
}
.differential-property-table {
margin: 12px;
background: {$lightgreybackground};
border: 1px solid {$lightblueborder};
border-bottom: 1px solid {$blueborder};
}
.differential-property-table td em {
color: {$lightgreytext};
}
.differential-property-table td.oval {
background: #ffd0d0;
width: 50%;
}
.differential-property-table td.nval {
background: #d0ffd0;
width: 50%;
}
tr.differential-inline-hidden {
display: none;
}
tr.differential-inline-loading {
opacity: 0.5;
}
.differential-review-stage {
position: relative;
}
.diff-banner {
position: fixed;
top: 0;
left: 0;
right: 0;
background: {$page.content};
box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1);
border-bottom: 1px solid {$lightgreyborder};
padding: 8px 18px;
vertical-align: middle;
font-weight: bold;
font-size: {$biggerfontsize};
line-height: 28px;
}
.diff-banner-path {
color: {$greytext};
}
.diff-banner-buttons .button {
margin-left: 8px;
}
.diff-banner-has-unsaved,
.diff-banner-has-unsubmitted,
.diff-banner-has-draft-done {
background: {$gentle.highlight};
}
.diff-banner-buttons {
float: right;
}
+
+/* In Firefox, making the table unselectable and then making cells selectable
+does not work: the cells remain unselectable. Narrowly mark the cells as
+unselectable. */
+
+.differential-diff.copy-l > tbody > tr > td,
+.differential-diff.copy-r > tbody > tr > td {
+ -moz-user-select: none;
+ -ms-user-select: none;
+ -webkit-user-select: none;
+ user-select: none;
+}
+
+.differential-diff.copy-l > tbody > tr > td:nth-child(2) {
+ -moz-user-select: auto;
+ -ms-user-select: auto;
+ -webkit-user-select: auto;
+ user-select: auto;
+}
+
+.differential-diff.copy-l > tbody > tr > td.show-more:nth-child(2) {
+ -moz-user-select: none;
+ -ms-user-select: none;
+ -webkit-user-select: none;
+ user-select: none;
+}
+
+.differential-diff.copy-r > tbody > tr > td:nth-child(5) {
+ -moz-user-select: auto;
+ -ms-user-select: auto;
+ -webkit-user-select: auto;
+ user-select: auto;
+}
+
+.differential-diff.copy-l > tbody > tr.inline > td,
+.differential-diff.copy-r > tbody > tr.inline > td {
+ -moz-user-select: none;
+ -ms-user-select: none;
+ -webkit-user-select: none;
+ user-select: none;
+}
diff --git a/webroot/rsrc/css/application/differential/core.css b/webroot/rsrc/css/application/differential/core.css
index 2dcc02bb1..893cfb34a 100644
--- a/webroot/rsrc/css/application/differential/core.css
+++ b/webroot/rsrc/css/application/differential/core.css
@@ -1,29 +1,21 @@
/**
* @provides differential-core-view-css
*/
.differential-primary-pane {
margin-top: -20px;
}
.differential-panel {
padding: 16px;
}
.differential-panel h1 {
border-bottom: 1px solid #aaaa99;
padding-bottom: 8px;
margin-bottom: 8px;
}
-.differential-unselectable tr td:nth-of-type(1) {
- -moz-user-select: -moz-none;
- -khtml-user-select: none;
- -webkit-user-select: none;
- -ms-user-select: none;
- user-select: none;
-}
-
.differential-content-hidden {
margin: 0 0 24px 0;
}
diff --git a/webroot/rsrc/css/application/project/project-card-view.css b/webroot/rsrc/css/application/project/project-card-view.css
index b960d55ce..cce4789ef 100644
--- a/webroot/rsrc/css/application/project/project-card-view.css
+++ b/webroot/rsrc/css/application/project/project-card-view.css
@@ -1,176 +1,190 @@
/**
* @provides project-card-view-css
*/
.project-card-view {
margin: 0 12px 16px 0;
text-align: left;
background: {$page.content};
border: 1px solid {$lightblueborder};
border-radius: 3px;
box-shadow: {$dropshadow};
width: 420px;
position: relative;
}
.project-card-view .phui-header-shell {
margin: 0;
padding: 12px 12px 4px 12px;
border: none;
border-radius: 3px;
}
.project-card-view .phui-header-shell .phui-header-image {
border: 3px solid {$page.content};
border-radius: 3px;
background-color: {$page.content};
}
.project-card-view .phui-header-shell .phui-header-header {
font-size: 18px;
width: 290px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: block;
}
.project-card-view .project-card-image {
+ position: absolute;
height: 140px;
width: 140px;
- margin: 6px;
+ top: 6px;
+ left: 6px;
border-radius: 3px;
}
.project-card-view .project-card-image-href {
- display: inline-block;
+ display: block;
}
.project-card-view .project-card-item div {
display: inline;
}
+.project-card-inner {
+ position: relative;
+}
+
+.people-card-view .project-card-inner {
+ padding: 6px;
+ min-height: 140px;
+}
+
.project-card-view .project-card-item {
margin-bottom: 2px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.project-card-view .project-card-item-text {
color: {$greytext};
}
.project-card-view .project-card-item-icon {
width: 20px;
}
.project-card-view .project-card-header {
- position: absolute;
- top: 12px;
- left: 158px;
+ margin-top: 6px;
+ margin-left: 152px;
+ overflow: hidden;
}
.project-card-header .project-card-name {
font-size: 20px;
font-weight: bold;
color: {$blacktext};
margin-bottom: 2px;
text-overflow: ellipsis;
white-space: nowrap;
width: 250px;
overflow: hidden;
}
.project-card-header .project-card-username {
font-size: 14px;
color: {$bluetext};
margin-bottom: 12px;
}
.project-card-view .phui-header-shell .phui-header-col1 {
vertical-align: top;
width: 64px;
}
.project-card-view .phui-header-subheader {
font-size: {$normalfontsize};
margin-top: 12px;
padding-bottom: 12px;
}
.project-card-view .phui-header-header .phui-tag-view {
display: block;
font-weight: normal;
color: {$bluetext};
font-size: {$normalfontsize};
font-family: {$fontfamily};
margin-top: 8px;
}
.people-card-view .phui-header-subheader .phui-tag-core {
text-overflow: ellipsis;
white-space: nowrap;
max-width: 232px;
overflow: hidden;
display: inline-block;
}
.project-card-view .phui-header-header .phui-tag-view .phui-tag-core {
padding: 0;
}
.project-card-view .phui-header-header .phui-tag-view .phui-icon-view {
margin-left: 0;
color: {$bluetext};
}
.project-card-view .project-card-body {
padding: 0 12px 12px 12px;
color: {$darkbluetext};
}
/* Colors */
.project-card-view.project-card-red .phui-header-shell {
background: linear-gradient(to bottom,
{$sh-redbackground} 42px, {$page.content} 42px);
}
.project-card-view.project-card-orange .phui-header-shell {
background: linear-gradient(to bottom,
{$sh-orangebackground} 42px, {$page.content} 42px);
}
.project-card-view.project-card-yellow .phui-header-shell {
background: linear-gradient(to bottom,
{$sh-yellowbackground} 42px, {$page.content} 42px);
}
.project-card-view.project-card-green .phui-header-shell {
background: linear-gradient(to bottom,
{$sh-greenbackground} 42px, {$page.content} 42px);
}
.project-card-view.project-card-blue .phui-header-shell {
background: linear-gradient(to bottom,
{$sh-bluebackground} 42px, {$page.content} 42px);
}
.project-card-view.project-card-indigo .phui-header-shell {
background: linear-gradient(to bottom,
{$sh-indigobackground} 42px, {$page.content} 42px);
}
.project-card-view.project-card-violet .phui-header-shell {
background: linear-gradient(to bottom,
{$sh-violetbackground} 42px, {$page.content} 42px);
}
.project-card-view.project-card-pink .phui-header-shell {
background: linear-gradient(to bottom,
{$sh-pinkbackground} 42px, {$page.content} 42px);
}
.project-card-view.project-card-grey .phui-header-shell,
.project-card-view.project-card-checkered .phui-header-shell {
background: linear-gradient(to bottom,
{$sh-greybackground} 42px, {$page.content} 42px);
}
diff --git a/webroot/rsrc/css/application/project/project-triggers.css b/webroot/rsrc/css/application/project/project-triggers.css
new file mode 100644
index 000000000..9b3ce8e46
--- /dev/null
+++ b/webroot/rsrc/css/application/project/project-triggers.css
@@ -0,0 +1,38 @@
+/**
+ * @provides project-triggers-css
+ */
+
+.trigger-rules-table {
+ margin: 16px 0;
+ border-collapse: separate;
+ border-spacing: 0 4px;
+}
+
+.trigger-rules-table tr {
+ background: {$bluebackground};
+}
+
+.trigger-rules-table td {
+ padding: 6px 4px;
+ vertical-align: middle;
+}
+
+.trigger-rules-table td.type-cell {
+ padding-left: 6px;
+}
+
+.trigger-rules-table td.remove-column {
+ padding-right: 6px;
+}
+
+.trigger-rules-table td.invalid-cell {
+ padding-left: 12px;
+}
+
+.trigger-rules-table td.invalid-cell .phui-icon-view {
+ margin-right: 4px;
+}
+
+.trigger-rules-table td.value-cell {
+ width: 100%;
+}
diff --git a/webroot/rsrc/css/core/syntax.css b/webroot/rsrc/css/core/syntax.css
index cfc82da09..90f2981ba 100644
--- a/webroot/rsrc/css/core/syntax.css
+++ b/webroot/rsrc/css/core/syntax.css
@@ -1,31 +1,37 @@
/**
* @provides syntax-highlighting-css
* @requires syntax-default-css
*/
.remarkup-code .uu { /* Forbidden Unicode */
color: #aa0066;
}
.remarkup-code td > span {
display: inline;
word-break: break-all;
}
.remarkup-code .rbw_r { color: red; }
.remarkup-code .rbw_o { color: orange; }
.remarkup-code .rbw_y { color: yellow; }
.remarkup-code .rbw_g { color: green; }
.remarkup-code .rbw_b { color: blue; }
.remarkup-code .rbw_i { color: indigo; }
.remarkup-code .rbw_v { color: violet; }
span.crossreference-item {
background: {$lightyellow};
border-bottom: 1px solid {$yellow};
cursor: help;
}
.remarkup-code .invisible {
color: #222222;
background: #dddddd;
}
+
+.suspicious-character {
+ background: #ff7700;
+ color: #ffffff;
+ cursor: default;
+}
diff --git a/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css b/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css
index a793c018c..2d2163f9e 100644
--- a/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css
+++ b/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css
@@ -1,79 +1,99 @@
/**
* @provides phui-oi-big-ui-css
* @requires phui-oi-list-view-css
*/
.phui-oi-list-big ul.phui-oi-list-view {
margin: 0;
padding: 20px;
}
.phui-oi-list-big .phui-oi-no-bar .phui-oi-frame {
border: 0;
}
.phui-oi-list-big .phui-oi-image-icon {
- margin: 8px 2px 12px;
+ margin: 12px 2px 12px;
+ text-align: center;
+}
+
+.phui-oi-list-big .phui-oi-image-icon .phui-icon-view {
+ position: relative;
}
.phui-oi-list-big a.phui-oi-link {
color: {$blacktext};
font-size: {$biggestfontsize};
}
.phui-oi-list-big .phui-oi-name {
padding-top: 6px;
}
.phui-oi-list-big .phui-oi-launch-button a.button {
font-size: {$normalfontsize};
padding: 3px 12px 4px;
}
.device-desktop .phui-oi-list-big .phui-oi {
- margin-bottom: 4px;
+ margin-bottom: 8px;
}
.phui-oi-list-big .phui-oi-col0 {
vertical-align: top;
padding: 0;
}
.phui-oi-list-big .phui-oi-status-icon {
padding: 5px;
}
.phui-oi-list-big .phui-oi-visited a.phui-oi-link {
color: {$violet};
}
.phui-box-white-config .phui-oi-list-big.phui-oi-list-view {
padding: 8px 8px 4px;
}
.phui-box-white-config .phui-oi-frame {
padding: 4px 8px 0;
}
.device-desktop .phui-box-white-config .phui-oi:hover .phui-oi-frame {
background-color: {$hoverblue};
border-radius: 3px;
}
+.phui-oi-list-big .phui-oi-frame {
+ padding: 2px 8px;
+}
+
+.phui-oi-list-big .phui-oi-linked-container {
+ border: 1px solid {$lightblueborder};
+ border-radius: 4px;
+ box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.035);
+}
+
+.phui-oi-list-big .phui-oi-disabled {
+ border-radius: 4px;
+ background: {$lightgreybackground};
+}
+
.device-desktop .phui-oi-linked-container {
cursor: pointer;
}
.device-desktop .phui-oi-linked-container:hover {
background-color: {$hoverblue};
- border-radius: 3px;
+ border-color: {$blueborder};
}
.device-desktop .phui-oi-linked-container a:hover {
text-decoration: none;
}
/* Spacing for InfoView inside an object item list, like MFA setup. */
.phui-oi .phui-info-view {
margin: 0 4px 4px;
}
diff --git a/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css b/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css
index 6f2421ca2..67d0682aa 100644
--- a/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css
+++ b/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css
@@ -1,722 +1,728 @@
/**
* @provides phui-oi-list-view-css
*/
.phui-oi {
border-left-width: 0;
}
ul.phui-oi-list-view {
padding: 8px;
list-style: none;
}
.device-desktop .phui-oi-list-view {
padding: 16px;
}
.phui-oi-list-view + .phui-oi-list-view {
padding-top: 0;
}
.phui-object-box .phui-oi-list-view .phui-oi {
margin: 0;
}
.phui-oi-list-view .phui-info-view {
margin: 0;
}
.phui-object-box .phui-oi-list-view .phui-info-view {
color: {$greytext};
border: none;
}
.phui-oi {
border-style: solid;
border-color: {$lightgreyborder};
margin: 5px 0;
overflow: hidden;
background: {$page.content};
margin-bottom: 4px;
}
.phui-oi .phui-icon-view {
display: inline-block;
}
.phui-oi-frame {
border-color: {$lightblueborder};
border-width: 1px 1px 1px 0;
border-style: solid;
position: relative;
min-height: 33px;
overflow: hidden;
}
.phui-oi-cover-image {
display: none;
}
.phui-oi-no-bar .phui-oi-frame {
border-width: 1px;
}
.device-desktop .phui-oi {
margin: 0 0 4px 0;
}
.phui-object-box .phui-oi-list-view {
margin: 0;
}
.phui-oi-status-icon {
font-weight: bold;
padding: 3px;
font-size: 16px;
}
.phui-oi-list-view .phui-oi-col0 .phui-icon-view {
width: 17px;
text-align: center;
overflow: visible;
position: relative;
left: -1px;
}
.phui-oi-name {
padding: 8px 8px 0;
white-space: nowrap;
word-wrap: break-word;
overflow: hidden;
text-overflow: ellipsis;
font-weight: bold;
-webkit-font-smoothing: antialiased;
}
.device-phone .phui-oi-name {
overflow: normal;
white-space: normal;
font-weight: bold;
}
.phui-oi-link {
display: inline;
}
.phui-oi-objname {
color: {$blacktext};
cursor: text;
font-weight: bold;
}
.phui-oi-content {
margin: 4px 8px 2px 0;
overflow: hidden;
}
.phui-oi-grippable {
cursor: move;
}
.device .phui-oi-grippable {
cursor: normal;
}
.phui-oi-grip {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 20px;
background: url('/rsrc/image/texture/grip.png') center center no-repeat;
}
.device .phui-oi-grip {
display: none;
}
.phui-oi-grippable .phui-oi-frame {
padding-left: 16px;
}
.device .phui-oi-grippable .phui-oi-frame {
padding-left: 0;
}
.phui-oi-list-header {
padding: 0 0 8px 0;
color: {$darkgreytext};
}
.phui-oi-table {
display: table;
table-layout: fixed;
width: 100%;
}
.phui-oi-table-row {
display: table-row;
}
.phui-oi-col0 {
width: 20px;
display: table-cell;
vertical-align: middle;
padding-left: 4px;
}
.device-phone .phui-oi-col0 {
vertical-align: top;
padding-top: 8px;
}
.phui-oi-col1 {
display: table-cell;
vertical-align: top;
}
.phui-oi-col2 {
width: 160px;
display: table-cell;
vertical-align: top;
}
.phui-oi-col2.phui-oi-side-column {
width: 200px;
}
.device-phone .phui-oi-col1,
.device-phone .phui-oi-col2 {
display: block;
width: auto;
}
/* - Item Actions --------------------------------------------------------------
Action buttons, like "Edit" and "Delete".
*/
.phui-oi-actions {
position: absolute;
right: 4px;
top: 4px;
bottom: 4px;
vertical-align: middle;
text-align: right;
}
.phui-oi-actions .phui-list-item-view {
float: right;
height: 100%;
width: 24px;
display: inline-block;
position: relative;
}
.phui-oi-actions .phui-list-item-href {
display: inline-block;
position: relative;
width: 24px;
height: 100%;
}
.device-desktop .phui-oi-actions .phui-list-item-href:hover {
background: {$hoverblue};
border-radius: 3px;
}
.phui-oi-actions .phui-list-item-icon {
width: 14px;
height: 14px;
position: absolute;
display: block;
top: 50%;
margin-top: -7px;
left: 3px;
}
.phui-oi-actions .phui-list-item-name {
display: none;
}
.phui-oi-with-1-actions .phui-oi-content-box {
margin-right: 28px;
overflow: hidden;
}
.phui-oi-with-2-actions .phui-oi-content-box {
margin-right: 54px;
overflow: hidden;
}
.phui-oi-with-3-actions .phui-oi-content-box {
margin-right: 76px;
overflow: hidden;
}
/* - Object Box List -----------------------------------------------------------
Tighter, stacking list when inside an Object Box
*/
.phui-object-box .phui-oi-list-view {
padding: 0;
border: none;
}
.phui-object-box .phui-oi-frame {
border-right: none;
}
.phui-object-box .phui-oi:last-child
.phui-oi-frame {
border-bottom: none;
}
/* - Subhead -------------------------------------------------------------------
Descriptive Text or Links under the main header, before attributes.
*/
.phui-oi-subhead {
color: {$greytext};
padding: 0 8px 6px;
}
.phui-oi-description {
display: none;
}
.phui-oi-description.phui-oi-description-reveal {
display: block;
}
.phui-oi-description-tag {
margin-left: 4px;
}
.phui-oi-description-tag:hover .phui-tag-core {
cursor: pointer;
background: {$darkgreybackground};
}
.phui-oi-description-tag .phui-tag-core {
border: none;
}
.phui-oi-description-tag.phui-tag-view .phui-icon-view {
margin: 2px;
}
/* - Attribute List ------------------------------------------------------------
Object attributes, commonly used to render created date, etc.
*/
.phui-oi-attributes {
padding: 0 8px 6px;
line-height: 18px;
min-height: 21px;
}
.phui-oi-attribute {
display: inline-block;
color: {$greytext};
vertical-align: top;
margin-right: 4px;
}
.phui-oi-attribute-spacer {
padding: 0 4px;
}
/* - Icons ---------------------------------------------------------------------
Icons, which show object state. On mobile, they are rendered without labels
to save space.
*/
.phui-object-icon-pane {
margin: 8px 0 4px;
}
.device-phone .phui-object-icon-pane {
margin: 0 0 4px;
}
.phui-oi-icons {
padding: 0 4px 0 0;
}
.device-phone .phui-oi-icons {
padding: 0 0 0 8px;
}
ul.phui-oi-icons {
margin: 0;
}
.phui-oi-icon {
vertical-align: middle;
font-size: {$smallerfontsize};
color: {$greytext};
text-align: right;
white-space: nowrap;
overflow: hidden;
min-height: 18px;
line-height: 18px;
}
.device-phone .phui-oi-icon {
text-align: left;
font-size: 13px;
}
/*
* Items with icon 'none' still have on mobile, thus creating a weird vertical
* margin for elements which follow
*/
.device-phone .phui-oi-icon .none {
display: none;
}
.phui-oi-icon-image {
width: 14px;
height: 14px;
font-size: 13px;
margin-right: 4px;
}
/* - Disabled ------------------------------------------------------------------
Disabled/inactive objects.
*/
.phui-oi.phui-oi-disabled .phui-oi-link,
.phui-oi.phui-oi-disabled .phui-oi-link a {
color: {$lightgreytext};
}
.phui-oi.phui-oi-disabled .phui-oi-frame {
border-color: #d7d7d7;
}
.phui-oi.phui-oi-disabled .phui-oi-objname {
color: {$greytext};
text-decoration: line-through;
}
.phui-oi.phui-oi-disabled .phui-oi-image {
opacity: .8;
-webkit-filter: grayscale(100%);
filter: grayscale(100%);
}
.phui-oi.phui-oi-disabled .phui-oi-attribute,
.phui-oi.phui-oi-disabled .phui-oi-attribute > .phui-icon-view {
color: {$lightgreytext};
}
/* - Effects -------------------------------------------------------------------
Effects like highlighted items.
*/
.phui-oi.phui-oi-highlighted {
background: {$sh-yellowbackground};
}
ul.phui-oi-list-view .phui-oi-highlighted
.phui-oi-frame {
border-color: {$sh-yellowborder};
}
.phui-oi-selected {
background: {$sh-bluebackground};
}
ul.phui-oi-list-view .phui-oi-selected
.phui-oi-frame {
border-color: {$sh-blueborder};
}
.phui-oi-forbidden {
background: {$sh-redbackground};
}
/* - Handle Icons --------------------------------------------------------------
Shows owners, reviewers, etc., using profile picture icons.
*/
.phui-oi-handle-icons {
bottom: 0;
right: 4px;
position: absolute;
}
.phui-oi-handle-icon {
width: 24px;
height: 24px;
display: inline-block;
background-size: 100%;
border-radius: 3px;
background-repeat: no-repeat;
}
/* - Bylines -------------------------------------------------------------------
Shows owners, authors, reviewers, etc., in text.
*/
.phui-oi-bylines {
padding: 0 4px 0 8px;
margin: 4px 0 8px;
font-size: {$smallerfontsize};
color: {$greytext};
text-align: right;
}
.phui-oi-byline {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.device-phone .phui-oi-bylines {
float: none;
text-align: left;
padding: 0 8px;
font-size: {$normalfontsize};
}
/* - Draggable List ------------------------------------------------------------
These classes are applied by and/or provided for use with JX.DraggableList.
*/
.drag-ghost {
position: relative;
background: {$sh-indigobackground};
border-radius: 3px;
margin-bottom: 4px;
border: 1px dashed {$sh-indigoborder};
}
.drag-dragging {
opacity: 0.25;
}
.drag-sending {
opacity: 0.5;
}
.drag-clone,
.drag-frame {
/* This allows mousewheel events to pass through the clone and frame while
they are being dragged. Without this, the mousewheel does not work during
a drag operation. */
pointer-events: none;
}
.drag-frame {
position: fixed;
overflow: hidden;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.drag-clone {
position: absolute;
list-style: none;
}
/* - Badges ---------------------------------------------------------------- */
.phui-oi-col0.phui-oi-badge {
width: 28px;
}
.phui-oi-col0.phui-oi-badge .phui-icon-view {
left: 0;
}
/* - Countdowns ------------------------------------------------------------ */
.phui-oi-col0.phui-oi-countdown {
width: 52px;
padding: 0;
}
.phui-oi-countdown .phui-oi-countdown-number {
border-right: 1px solid {$thinblueborder};
text-align: center;
color: {$bluetext};
}
/* - Dashboards ------------------------------------------------------------ */
.phui-object-box .phui-oi-list-view .phui-oi-frame {
border: none;
border-bottom: 1px solid {$thinblueborder};
}
.drag-clone.phui-oi-standard .phui-oi-frame {
border: none;
opacity: 0.8;
background: {$sh-bluebackground};
}
.phui-object-box .phui-oi-list-header {
font-size: {$normalfontsize};
color: {$darkbluetext};
border-top: 1px solid {$thinblueborder};
border-bottom: 1px solid {$thinblueborder};
padding: 8px 12px;
background-color: {$lightgreybackground};
}
.phui-object-box .phui-header-shell + .phui-oi-list-view .phui-oi-list-header,
.phui-object-box .phui-object-box-hidden-content + .phui-oi-list-view
.phui-oi-list-header,
.phui-object-box .phui-object-box-hidden-content + .phui-oi-list-header {
border-top: none;
}
.dashboard-pane .phui-oi-empty .phui-info-view {
border: none;
margin: 0;
}
.device-desktop .aphront-multi-column-fluid .aphront-multi-column-2-up
.aphront-multi-column-column-outer.third .phui-oi-col2 {
display: none;
}
/* - Launcher List ---------------------------------------------------------- */
.phui-oi-image-icon {
background: none;
width: 40px;
height: 40px;
margin: 8px 6px;
position: absolute;
}
.phui-oi-image-icon .phui-icon-view {
position: absolute;
width: 40px;
height: 40px;
font-size: 26px;
text-align: center;
line-height: 36px;
}
.phui-oi-image {
width: 40px;
height: 40px;
border-radius: 3px;
background-size: 100%;
margin: 8px 6px;
position: absolute;
}
.phui-oi-with-image-icon .phui-oi-frame,
.phui-oi-with-image .phui-oi-frame {
min-height: 52px;
}
.phui-oi-with-image-icon .phui-oi-content-box,
.phui-oi-with-image .phui-oi-content-box {
margin-left: 46px;
}
/* - Launcher Button -------------------------------------------------------- */
.phui-oi-col2.phui-oi-side-column {
text-align: right;
vertical-align: middle;
padding-right: 4px;
}
.device-phone .phui-oi-col2.phui-oi-side-column {
padding: 0 8px 8px;
text-align: left;
}
.phui-oi-col0.phui-oi-checkbox {
width: 28px;
text-align: center;
}
.phui-oi-selectable {
cursor: pointer;
user-select: none;
-webkit-user-select: none;
}
/* When the list selection state can be toggled on the client (as in the bulk
editor), keep the border color consistent to make the interaction feel more
robust. */
ul.phui-oi-list-view .phui-oi-selectable
.phui-oi-frame {
border-color: {$blueborder};
}
.differential-revision-size {
padding: 0 4px;
border-radius: 4px;
background: {$lightgreybackground};
cursor: pointer;
}
.differential-revision-size .phui-icon-view {
margin: 0 1px 0 1px;
font-size: 7px;
position: relative;
top: -2px;
color: {$lightbluetext};
}
.differential-revision-large {
background: {$sh-orangebackground};
}
/* NOTE: These are intentionally using nonstandard colors, see T13127. */
.differential-revision-large .phui-icon-view {
color: #e5ae7e;
}
.differential-revision-small {
background: #f2f7ff;
}
.differential-revision-small .phui-icon-view {
color: #6699ba;
}
+
+.phui-oi-tail {
+ text-align: center;
+ padding: 8px 0;
+ background: linear-gradient({$lightbluebackground}, #fff 66%, #fff);
+}
diff --git a/webroot/rsrc/css/phui/phui-action-list.css b/webroot/rsrc/css/phui/phui-action-list.css
index e7ee38a8b..3df4ff1b7 100644
--- a/webroot/rsrc/css/phui/phui-action-list.css
+++ b/webroot/rsrc/css/phui/phui-action-list.css
@@ -1,198 +1,215 @@
/**
* @provides phabricator-action-list-view-css
*/
.device .phabricator-action-list-view {
padding: 4px 0;
display: none;
}
!print .phabricator-action-list-view {
padding: 4px 0;
display: none;
}
.device .phuix-dropdown-menu .phabricator-action-list-view {
/* When an action list view appears inside a dropdown menu, don't hide it
by default. */
display: block;
padding: 0;
}
.device .phabricator-action-list-view.phabricator-action-list-toggle,
.device-desktop .phui-document-content
.phabricator-action-list-view.phabricator-action-list-toggle {
display: block;
width: 200px;
border: 1px solid {$lightgreyborder};
border-radius: 3px;
position: absolute;
right: 16px;
top: 42px;
background: #fff;
box-shadow: {$dropshadow};
padding: 4px 0;
}
.device-phone .phabricator-action-list-view.phabricator-action-list-toggle {
right: 8px;
top: 38px;
}
.phabricator-action-view {
position: relative;
}
.phabricator-action-view button.phabricator-action-view-item {
border: none;
background: transparent;
box-shadow: none;
outline: 0;
padding: 0;
margin: 0;
font-weight: normal;
width: 100%;
text-align: left;
text-shadow: none;
border-radius: 0;
color: {$anchor};
font: inherit;
display: inline;
min-width: 0;
}
.phabricator-action-view button.phabricator-action-view-item .phui-icon-view {
color: {$darkbluetext};
}
.phabricator-action-view button.phabricator-action-view-item,
.phabricator-action-view-item {
padding: 4px 8px 6px 8px;
display: block;
text-decoration: none;
color: {$darkbluetext};
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.action-has-icon button.phabricator-action-view-item,
.action-has-icon .phabricator-action-view-item {
padding: 4px 4px 4px 28px;
}
.device-desktop .phabricator-action-view-href:hover
.phabricator-action-view-item {
text-decoration: none;
background: rgba({$alphablue}, .08);
color: {$sky};
border-radius: 3px;
}
.device-desktop .phabricator-action-view-href:hover
.phabricator-action-view-icon {
color: {$sky};
}
.phabricator-action-view.action-item-red {
background-color: {$sh-redbackground};
}
+.phabricator-action-view.action-item-green {
+ background-color: {$sh-greenbackground};
+}
+
.phabricator-action-view.action-item-red .phabricator-action-view-item,
.phabricator-action-view.action-item-red .phabricator-action-view-icon {
color: {$sh-redtext};
}
+.phabricator-action-view.action-item-green .phabricator-action-view-item,
+.phabricator-action-view.action-item-green .phabricator-action-view-icon {
+ color: {$sh-greentext};
+}
+
.device-desktop .phabricator-action-view.action-item-red:hover
.phabricator-action-view-item,
.device-desktop .phabricator-action-view.action-item-red:hover
.phabricator-action-view-icon {
color: {$red};
}
+.device-desktop .phabricator-action-view.action-item-green:hover
+ .phabricator-action-view-item,
+.device-desktop .phabricator-action-view.action-item-green:hover
+ .phabricator-action-view-icon {
+ color: {$green};
+}
+
+
.phabricator-action-view-label .phabricator-action-view-item,
.phabricator-action-view-type-label .phabricator-action-view-item {
font-size: {$smallerfontsize};
font-weight: bold;
color: {$bluetext};
padding: 4px 8px 6px 8px;
display: block;
text-transform: uppercase;
-webkit-font-smoothing: antialiased;
}
.phabricator-action-view + .phabricator-action-view-label {
padding-top: 8px;
}
.phabricator-action-view-icon {
width: 14px;
height: 14px;
position: absolute;
top: 7px;
left: 8px;
text-align: center;
}
.phabricator-action-view-disabled .phabricator-action-view-item,
.phabricator-action-view-disabled button.phabricator-action-view-item {
color: {$lightgreytext};
}
.phabricator-action-view-selected {
background: {$sh-violetbackground};
border-radius: 3px;
}
.phabricator-action-view-selected:hover a {
text-decoration: none;
}
.phabricator-action-view button[disabled] {
opacity: 1.0;
}
.device-desktop .phabricator-action-view-disabled:hover
.phabricator-action-view-item,
.device-desktop .phabricator-action-view-disabled:hover
button.phabricator-action-view-item,
.device-desktop .phabricator-action-view-disabled:hover
.phabricator-action-view-icon,
.device-desktop .phabricator-action-view-disabled:hover
button.phabricator-action-view-icon {
color: {$lightgreytext};
}
.phabricator-action-view-type-divider {
margin-top: 8px;
border-top: 1px solid {$thinblueborder};
}
/******* Sub Menu *************************************************************/
.phabricator-action-view-submenu .caret-right {
position: absolute;
top: 8px;
right: 8px;
border-left-color: {$alphablue};
}
.phabricator-action-view-submenu .caret {
position: absolute;
top: 10px;
right: 8px;
border-top: 7px solid {$lightgreytext};
}
.phabricator-action-list-view .phabricator-action-view-submenu.phui-submenu-open
.phabricator-action-view-item {
background-color: rgba({$alphablue}, 0.07);
color: {$sky};
border-radius: 3px;
}
.phabricator-action-list-view .phabricator-action-view-submenu.phui-submenu-open
.phabricator-action-view-item .phui-icon-view {
color: {$sky};
}
diff --git a/webroot/rsrc/css/phui/phui-form-view.css b/webroot/rsrc/css/phui/phui-form-view.css
index 3368bcaaf..accce8681 100644
--- a/webroot/rsrc/css/phui/phui-form-view.css
+++ b/webroot/rsrc/css/phui/phui-form-view.css
@@ -1,580 +1,594 @@
/**
* @provides phui-form-view-css
*/
.phui-form-view {
padding: 16px;
}
.device-phone .phui-object-box .phui-form-view {
padding: 0;
}
.phui-form-view.phui-form-full-width {
padding: 0;
}
.phui-form-view label.aphront-form-label {
width: 19%;
height: 28px;
line-height: 28px;
float: left;
text-align: right;
font-weight: bold;
font-size: {$normalfontsize};
color: {$bluetext};
-webkit-font-smoothing: antialiased;
}
.device-phone .phui-form-view label.aphront-form-label,
.phui-form-full-width.phui-form-view label.aphront-form-label {
display: block;
float: none;
text-align: left;
width: 100%;
margin-bottom: 3px;
}
.aphront-form-input {
margin-left: 20%;
margin-right: 20%;
width: 60%;
}
.device-phone .aphront-form-input,
.device .aphront-form-input select,
.device .aphront-form-input pre,
.phui-form-full-width .aphront-form-input {
margin-left: 0%;
margin-right: 0%;
width: 100%;
}
.aphront-form-input *::-webkit-input-placeholder {
color:{$greytext} !important;
}
.aphront-form-input *::-moz-placeholder {
color:{$greytext} !important;
opacity: 1; /* Firefox nudges the opacity to 0.4 */
}
.aphront-form-input *:-ms-input-placeholder {
color:{$greytext} !important;
}
.aphront-form-error {
width: 18%;
float: right;
color: {$red};
font-weight: bold;
padding-top: 5px;
}
.aphront-form-label .aphront-form-error {
display: none;
}
.aphront-dialog-body .phui-form-view {
padding: 0;
}
.device-phone .aphront-form-error,
.phui-form-full-width .aphront-form-error {
display: none;
}
.device-phone .aphront-form-label .aphront-form-error,
.phui-form-full-width .aphront-form-label .aphront-form-error {
display: block;
float: right;
padding: 0;
width: auto;
}
.device-phone .aphront-form-drag-and-drop-upload {
display: none;
}
.aphront-form-required {
font-weight: normal;
color: {$lightgreytext};
font-size: {$smallestfontsize};
-webkit-font-smoothing: antialiased;
}
.aphront-form-input input[type="text"],
.aphront-form-input input[type="password"] {
width: 100%;
}
.aphront-form-cvc-input input {
width: 64px;
}
.aphront-form-input textarea {
display: block;
width: 100%;
box-sizing: border-box;
height: 12em;
}
.aphront-form-control {
padding: 4px;
}
.device-phone .aphront-form-control {
padding: 4px 8px 8px;
}
.phui-form-full-width .aphront-form-control {
padding: 4px 0;
}
.aphront-form-control-submit button,
.aphront-form-control-submit a.button,
.aphront-form-control-submit input[type="submit"] {
float: right;
margin: 4px 0 0 8px;
}
.aphront-form-control-textarea textarea.aphront-textarea-very-short {
height: 44px;
}
.aphront-form-control-textarea textarea.aphront-textarea-very-tall {
height: 24em;
}
.phui-form-view .aphront-form-caption {
font-size: {$smallerfontsize};
color: {$bluetext};
padding: 8px 0;
margin-right: 20%;
margin-left: 20%;
-webkit-font-smoothing: antialiased;
line-height: 16px;
}
.device-phone .phui-form-view .aphront-form-caption,
.phui-form-full-width .phui-form-view .aphront-form-caption {
margin: 0;
}
.aphront-form-instructions {
width: 60%;
margin-left: 20%;
padding: 12px 4px;
color: {$darkbluetext};
}
.device .aphront-form-instructions,
.phui-form-full-width .aphront-form-instructions {
width: auto;
margin: 0;
padding: 12px 8px 8px;
}
.aphront-form-important {
margin: .5em 0;
background: #ffffdd;
padding: .5em 1em;
}
.aphront-form-important code {
display: block;
padding: .25em;
margin: .5em 2em;
}
.aphront-form-control-markup .aphront-form-input {
font-size: {$normalfontsize};
padding: 3px 0;
}
.aphront-form-control-static .aphront-form-input {
line-height: 28px;
}
.aphront-form-control-togglebuttons .aphront-form-input {
padding: 2px 0 0 0;
}
table.aphront-form-control-radio-layout,
table.aphront-form-control-checkbox-layout {
margin-top: 4px !important;
font-size: {$normalfontsize};
}
table.aphront-form-control-radio-layout th {
padding-left: 8px;
padding-bottom: 8px;
font-weight: bold;
color: {$darkgreytext};
}
table.aphront-form-control-checkbox-layout th {
padding-top: 2px;
padding-left: 8px;
padding-bottom: 4px;
color: {$darkgreytext};
}
.aphront-form-control-radio-layout td input,
.aphront-form-control-checkbox-layout td input {
margin-top: 4px;
width: auto;
}
.aphront-form-control-radio-layout label.disabled,
.aphront-form-control-checkbox-layout label.disabled {
color: {$greytext};
}
.aphront-form-radio-caption {
margin-top: 4px;
font-size: {$smallerfontsize};
font-weight: normal;
color: {$bluetext};
}
.aphront-form-control-image span {
margin: 0 4px 0 2px;
}
.aphront-form-control-image .default-image {
display: inline;
width: 12px;
}
.aphront-form-input hr {
border: none;
background: #bbbbbb;
height: 1px;
position: relative;
}
.phui-form-inset {
margin: 12px 0;
padding: 8px;
background: #f7f9fd;
border: 1px solid {$lightblueborder};
border-radius: 3px;
}
.phui-form-inset h1 {
color: {$bluetext};
padding-bottom: 8px;
margin-bottom: 8px;
font-size: {$biggerfontsize};
border-bottom: 1px solid {$thinblueborder};
}
.aphront-form-drag-and-drop-file-list {
width: 400px;
}
.drag-and-drop-instructions {
color: {$darkgreytext};
font-size: {$smallestfontsize};
padding: 6px 8px;
}
.drag-and-drop-file-target {
border: 1px dashed #bfbfbf;
padding-top: 12px;
padding-bottom: 12px;
}
body .phui-form-view .remarkup-assist-textarea.aphront-textarea-drag-and-drop {
background: {$sh-greenbackground};
border: 1px solid {$sh-greenborder};
}
.aphront-form-crop .crop-box {
cursor: move;
overflow: hidden;
}
.aphront-form-crop .crop-box .crop-image {
position: relative;
top: 0px;
left: 0px;
}
.calendar-button {
display: inline;
padding: 8px 4px;
margin: 2px 8px 2px 2px;
position: relative;
}
.aphront-form-date-container {
position: relative;
display: inline;
}
.aphront-form-date-container select {
margin: 2px;
display: inline;
}
.aphront-form-date-container input.aphront-form-date-enabled-input {
width: auto;
display: inline;
margin-right: 8px;
font-size: 16px;
}
.aphront-form-date-container .aphront-form-time-input-container,
.aphront-form-date-container .aphront-form-date-input-container {
position: relative;
display: inline-block;
width: 7em;
}
.aphront-form-date-container input.aphront-form-time-input,
.aphront-form-date-container input.aphront-form-date-input {
width: 7em;
}
.aphront-form-time-input-container div.jx-typeahead-results a.jx-result {
border: none;
}
.phui-time-typeahead-value {
padding: 4px;
}
.fancy-datepicker {
position: absolute;
width: 240px;
}
.device .fancy-datepicker {
width: 100%;
}
.fancy-datepicker-core {
width: 240px;
margin: 0 auto;
padding: 1px;
font-size: {$smallerfontsize};
text-align: center;
}
.fancy-datepicker-core .month-table,
.fancy-datepicker-core .day-table {
margin: 0 auto;
border-collapse: separate;
border-spacing: 1px;
width: 100%;
}
.fancy-datepicker-core .month-table {
margin-bottom: 6px;
font-size: {$normalfontsize};
background-color: {$hoverblue};
border-radius: 2px;
}
.fancy-datepicker-core .month-table td.lrbutton {
width: 18%;
color: {$lightbluetext};
}
.fancy-datepicker-core .month-table td {
padding: 4px;
font-weight: bold;
color: {$bluetext};
}
.fancy-datepicker-core .month-table td.lrbutton:hover {
border-radius: 2px;
background: {$hoverselectedblue};
color: {$darkbluetext};
}
.fancy-datepicker-core .day-table td {
overflow: hidden;
vertical-align: center;
text-align: center;
border: 1px solid {$thinblueborder};
padding: 4px 0;
}
.fancy-datepicker .fancy-datepicker-core .day-table td.day:hover {
background-color: {$hoverblue};
border-color: {$lightblueborder};
}
.fancy-datepicker-core .day-table td.day-placeholder {
border-color: transparent;
background: transparent;
}
.fancy-datepicker-core .day-table td.weekend {
color: {$lightgreytext};
border-color: {$lightgreyborder};
background: {$lightgreybackground};
}
.fancy-datepicker-core .day-table td.day-name {
background: transparent;
border: 1px transparent;
vertical-align: bottom;
color: {$lightgreytext};
}
.fancy-datepicker-core .day-table td.today {
background: {$greybackground};
border-color: {$greyborder};
color: {$darkgreytext};
font-weight: bold;
}
.fancy-datepicker-core .day-table td.datepicker-selected {
background: {$lightgreen};
border-color: {$green};
color: {$green};
}
.fancy-datepicker-core td {
cursor: pointer;
}
.fancy-datepicker-core td.novalue {
cursor: inherit;
}
.picker-open .calendar-button .phui-icon-view {
color: {$sky};
}
.fancy-datepicker-core {
background-color: white;
border: 1px solid {$lightgreyborder};
box-shadow: {$dropshadow};
border-radius: 3px;
}
/* When the activation checkbox for the control is toggled off, visually
disable the individual controls. We don't actually use the "disabled" property
because we still want the values to submit. This is just a visual hint that
the controls won't be used. The controls themselves are still live, work
properly, and submit values. */
.datepicker-disabled select,
.datepicker-disabled .calendar-button,
.datepicker-disabled input[type="text"] {
opacity: 0.5;
}
.aphront-form-date-container.no-time .aphront-form-time-input{
display: none;
}
.login-to-comment {
margin: 12px;
}
.phui-form-divider hr {
height: 1px;
border: 0;
background: {$thinblueborder};
width: 85%;
margin: 15px auto;
}
.recaptcha_only_if_privacy {
display: none;
}
.phabricator-standard-custom-field-header {
font-size: 16px;
color: {$bluetext};
border-bottom: 1px solid {$lightbluetext};
padding: 16px 0 4px;
margin-bottom: 4px;
}
.device-desktop .text-with-submit-control-outer-bounds {
position: relative;
}
.device-desktop .text-with-submit-control-text-bounds {
position: absolute;
left: 0;
right: 184px;
}
.device-desktop .text-with-submit-control-submit-bounds {
text-align: right;
}
.device-desktop .text-with-submit-control-submit {
width: 180px;
}
.phui-form-iconset-table td {
vertical-align: middle;
padding: 4px 0;
}
.phui-form-iconset-table .phui-form-iconset-button-cell {
padding: 4px 8px;
}
.aphront-form-preview-hidden {
opacity: 0.5;
}
.aphront-form-error .phui-icon-view {
float: right;
color: {$lightgreyborder};
font-size: 20px;
}
.device-desktop .aphront-form-error .phui-icon-view:hover {
color: {$red};
}
.phui-form-static-action {
height: 28px;
line-height: 28px;
color: {$bluetext};
}
.phuix-form-checkbox-action {
padding: 4px;
color: {$bluetext};
}
.phuix-form-checkbox-action input[type=checkbox] {
margin: 4px 0;
}
.phuix-form-checkbox-label {
margin-left: 4px;
}
.phui-form-timer-icon {
width: 28px;
height: 28px;
padding: 4px;
font-size: 18px;
background: {$greybackground};
border-radius: 4px;
text-align: center;
vertical-align: middle;
text-shadow: 1px 1px rgba(0, 0, 0, 0.05);
}
.phui-form-timer-content {
padding: 4px 8px;
color: {$darkgreytext};
vertical-align: middle;
}
.mfa-form-enroll-button {
text-align: center;
}
+
+.phui-form-timer-updated {
+ animation: phui-form-timer-fade-in 2s linear;
+}
+
+
+@keyframes phui-form-timer-fade-in {
+ 0% {
+ background-color: {$lightyellow};
+ }
+ 100% {
+ background-color: transparent;
+ }
+}
diff --git a/webroot/rsrc/css/phui/phui-header-view.css b/webroot/rsrc/css/phui/phui-header-view.css
index 6cafb3e33..6a096af76 100644
--- a/webroot/rsrc/css/phui/phui-header-view.css
+++ b/webroot/rsrc/css/phui/phui-header-view.css
@@ -1,388 +1,398 @@
/**
* @provides phui-header-view-css
*/
.phui-header-shell {
border-bottom: 1px solid {$thinblueborder};
overflow: hidden;
padding: 0 4px 12px;
}
.phui-header-view {
display: table;
width: 100%
}
.phui-header-row {
display: table-row;
}
.phui-header-col1 {
display: table-cell;
vertical-align: middle;
width: 62px;
}
.phui-header-col2 {
display: table-cell;
vertical-align: middle;
word-break: break-word;
}
.phui-header-col3 {
display: table-cell;
vertical-align: middle;
}
body .phui-header-shell.phui-header-no-background {
background-color: transparent;
border: none;
}
body .phui-header-shell.phui-bleed-header {
background-color: #fff;
border-bottom: 1px solid {$thinblueborder};
width: auto;
margin: 16px;
}
body .phui-header-shell.phui-bleed-header
.phui-header-view {
padding: 8px 24px 8px 0;
color: {$darkbluetext};
}
.phui-header-shell + .phabricator-form-view {
border-top-width: 0;
}
.phui-property-list-view + .diviner-document-section {
margin-top: -1px;
}
.phui-header-view {
position: relative;
font-size: {$normalfontsize};
}
.phui-header-header {
font-size: 16px;
line-height: 24px;
color: {$darkbluetext};
}
.phui-header-header .phui-header-icon {
margin-right: 8px;
color: {$lightbluetext};
/* This allows the header text to be triple-clicked to select it in Firefox,
see T10905 for discussion. */
display: inline;
}
.phui-object-box .phui-header-tall .phui-header-header,
.phui-document-view .phui-header-tall .phui-header-header {
font-size: 18px;
}
.phui-header-view .phui-header-header a {
color: {$darkbluetext};
}
.phui-box-blue-property .phui-header-view .phui-header-header a {
color: {$bluetext};
}
.device-desktop .phui-header-view .phui-header-header a:hover {
text-decoration: none;
color: {$blue};
}
.phui-header-view .phui-header-action-links {
float: right;
}
.phui-object-box .phui-header-view .phui-header-action-links {
margin-right: 4px;
font-size: {$normalfontsize};
}
.phui-header-action-link {
margin-bottom: 4px;
margin-top: 4px;
float: right;
}
.device-phone .phui-header-action-link .phui-button-text {
visibility: hidden;
width: 0;
margin-left: 8px;
}
.device-phone .phui-header-action-link.button .phui-icon-view {
width: 12px;
text-align: center;
}
.phui-header-divider {
margin: 0 4px;
font-weight: normal;
color: {$lightbluetext};
}
.phui-header-tags {
margin-left: 12px;
font-size: {$normalfontsize};
}
.phui-header-tags .phui-tag-view {
margin-left: 4px;
}
.phui-header-image {
display: inline-block;
background-repeat: no-repeat;
background-size: 100%;
width: 50px;
height: 50px;
border-radius: 3px;
}
.phui-header-image-href {
position: relative;
display: block;
}
.phui-header-image-edit {
display: none;
}
.device-desktop .phui-header-image-href:hover .phui-header-image-edit {
display: block;
position: absolute;
left: 0;
background: rgba({$alphablack},0.4);
color: #fff;
font-weight: normal;
bottom: 4px;
padding: 4px 8px;
font-size: 12px;
}
.device-desktop .phui-header-image-edit:hover {
text-decoration: underline;
}
.phui-header-subheader {
font-weight: normal;
font-size: {$biggerfontsize};
margin-top: 8px;
}
.phui-header-subheader .phui-icon-view {
margin-right: 4px;
}
.phui-header-subheader .phui-tag-view span.phui-icon-view,
.phui-header-subheader .policy-header-callout span.phui-icon-view {
display: inline-block;
margin: -2px 4px -2px 0;
font-size: 15px;
}
.phui-header-subheader,
.phui-header-subheader .policy-link {
color: {$darkbluetext};
}
.policy-header-callout,
.phui-header-subheader .phui-tag-core {
padding: 3px 9px;
border-radius: 3px;
background: rgba({$alphablue}, 0.1);
margin-right: 8px;
-webkit-font-smoothing: auto;
border-color: transparent;
}
.phui-header-subheader .phui-tag-view,
.phui-header-subheader .phui-tag-type-shade .phui-tag-core {
border: none;
font-weight: normal;
-webkit-font-smoothing: auto;
}
.policy-header-callout.policy-adjusted-weaker {
background: {$sh-greenbackground};
}
.policy-header-callout.policy-adjusted-weaker .policy-link,
.policy-header-callout.policy-adjusted-weaker .phui-icon-view {
color: {$sh-greentext};
}
.policy-header-callout.policy-adjusted-stronger {
background: {$sh-redbackground};
}
.policy-header-callout.policy-adjusted-stronger .policy-link,
.policy-header-callout.policy-adjusted-stronger .phui-icon-view {
color: {$sh-redtext};
}
.policy-header-callout.policy-adjusted-different {
background: {$sh-orangebackground};
}
.policy-header-callout.policy-adjusted-different .policy-link,
.policy-header-callout.policy-adjusted-different .phui-icon-view {
color: {$sh-orangetext};
}
.policy-header-callout.policy-adjusted-special {
background: {$sh-indigobackground};
}
.policy-header-callout.policy-adjusted-special .policy-link,
.policy-header-callout.policy-adjusted-special .phui-icon-view {
color: {$sh-indigotext};
}
+.policy-header-callout.policy-adjusted-locked {
+ background: {$sh-pinkbackground};
+}
+
+.policy-header-callout.policy-adjusted-locked .policy-link,
+.policy-header-callout.policy-adjusted-locked .phui-icon-view {
+ color: {$sh-pinktext};
+}
+
+
.policy-header-callout .policy-space-container {
font-weight: bold;
color: {$sh-redtext};
}
.policy-header-callout .policy-tier-separator {
padding: 0 0 0 4px;
color: {$lightgreytext};
}
.phui-header-action-links .phui-mobile-menu {
display: none;
}
.device .phui-header-action-links .phui-mobile-menu {
display: inline-block;
}
.phui-header-action-list {
float: right;
}
.phui-header-action-list li {
margin: 0 0 0 8px;
float: right;
}
.phui-header-action-list .phui-header-action-item .phui-icon-view {
height: 18px;
width: 16px;
font-size: 16px;
line-height: 20px;
display: block;
}
.spaces-name {
color: {$lightbluetext};
}
.phui-object-box .phui-header-tall .spaces-name {
font-size: 18px;
}
.spaces-name .phui-handle,
.spaces-name a.phui-handle,
.phui-profile-header.phui-header-shell .spaces-name .phui-handle {
color: {$sh-redtext};
}
.device-desktop .spaces-name a.phui-handle:hover {
color: {$sh-redtext};
text-decoration: underline;
}
/*** Profile Header ***********************************************************/
.phui-profile-header {
padding: 24px 20px 20px 24px;
}
.device-phone .phui-profile-header {
padding: 12px;
}
.phui-profile-header.phui-header-shell {
margin: 0;
border: none;
}
.phui-profile-header .phui-header-image {
height: 80px;
width: 80px;
}
.phui-profile-header .phui-header-col1 {
width: 96px;
}
.phui-profile-header .phui-header-subheader {
margin-top: 12px;
}
.phui-profile-header.phui-header-shell .phui-header-header {
font-size: 24px;
color: {$blacktext};
}
.phui-profile-header.phui-header-shell .phui-header-header a {
color: {$blacktext};
}
.phui-header-view .phui-tag-indigo a {
color: {$sh-indigotext};
}
.phui-policy-section-view {
margin-bottom: 24px;
}
.phui-policy-section-view-header {
background: {$bluebackground};
border-bottom: 1px solid {$lightblueborder};
padding: 4px 8px;
color: {$darkbluetext};
margin-bottom: 8px;
}
.phui-policy-section-view-header-text {
font-weight: bold;
}
.phui-policy-section-view-header .phui-icon-view {
margin-right: 8px;
}
.phui-policy-section-view-link {
float: right;
}
.phui-policy-section-view-link .phui-icon-view {
color: {$bluetext};
}
.phui-policy-section-view-hint {
color: {$greytext};
background: {$lightbluebackground};
padding: 8px;
}
.phui-policy-section-view-body {
padding: 0 12px;
}
.phui-policy-section-view-inactive-rule {
color: {$greytext};
}
diff --git a/webroot/rsrc/css/phui/phui-icon.css b/webroot/rsrc/css/phui/phui-icon.css
index 4108074b0..5436bb04b 100644
--- a/webroot/rsrc/css/phui/phui-icon.css
+++ b/webroot/rsrc/css/phui/phui-icon.css
@@ -1,185 +1,206 @@
/**
* @provides phui-icon-view-css
*/
.phui-icon-example .phui-icon-view {
display: inline-block;
vertical-align: top;
}
.phui-icon-view.sprite-tokens {
height: 18px;
width: 18px;
display: inline-block;
vertical-align: top;
}
.phui-icon-view.sprite-login {
height: 28px;
width: 28px;
}
.phui-icon-view.phuihead-medium {
height: 50px;
width: 50px;
}
.phui-icon-view.phuihead-small {
height: 35px;
width: 35px;
background-size: 35px;
}
.phui-icon-has-text:before {
margin-right: 6px;
}
a.phui-icon-view:hover {
text-decoration: none;
color: {$sky};
}
img.phui-image-disabled {
opacity: .8;
-webkit-filter: grayscale(100%);
filter: grayscale(100%);
}
.phui-icon-view.bluetext {
color: {$bluetext};
}
.phui-icon-view.invisible {
visibility: hidden;
}
/* - Icon in a Circle ------------------------------------------------------- */
.phui-icon-circle {
border: 1px solid {$lightblueborder};
border-radius: 24px;
height: 24px;
width: 24px;
text-align: center;
display: inline-block;
cursor: pointer;
background: transparent;
padding: 0;
position: relative;
}
.phui-icon-circle.circle-medium {
height: 36px;
width: 36px;
border-radius: 36px;
}
.phui-icon-circle.phui-icon-circle-state {
border-color: transparent;
background-color: {$bluebackground};
}
.phui-icon-circle.phui-icon-circle-state .phui-icon-circle-icon {
color: {$bluetext};
font-size: 16px;
}
a.phui-icon-circle.phui-icon-circle-state:hover {
border-color: transparent !important;
}
.phui-icon-circle .phui-icon-circle-icon {
height: 24px;
width: 24px;
font-size: 11px;
line-height: 24px;
color: {$lightblueborder};
cursor: pointer;
}
.phui-icon-circle.circle-medium .phui-icon-circle-icon {
font-size: 18px;
line-height: 36px;
}
a.phui-icon-circle.hover-sky:hover {
text-decoration: none;
border-color: {$sky};
cursor: pointer;
}
a.phui-icon-circle.hover-sky:hover .phui-icon-view {
color: {$sky};
}
a.phui-icon-circle.hover-violet:hover {
text-decoration: none;
border-color: {$violet};
cursor: pointer;
}
a.phui-icon-circle.hover-violet:hover .phui-icon-view {
color: {$violet};
}
a.phui-icon-circle.hover-pink:hover {
text-decoration: none;
border-color: {$pink};
cursor: pointer;
}
a.phui-icon-circle.hover-pink:hover .phui-icon-view {
color: {$pink};
}
a.phui-icon-circle.hover-green:hover {
text-decoration: none;
border-color: {$green};
cursor: pointer;
}
a.phui-icon-circle.hover-green:hover .phui-icon-view {
color: {$green};
}
a.phui-icon-circle.hover-red:hover {
text-decoration: none;
border-color: {$red};
cursor: pointer;
}
a.phui-icon-circle.hover-red:hover .phui-icon-view {
color: {$red};
}
.phui-icon-circle .phui-icon-view.phui-icon-circle-state-icon {
position: absolute;
width: 14px;
height: 14px;
display: inline-block;
font-size: 12px;
right: -3px;
top: -4px;
text-shadow:
-1px -1px 0 #fff,
1px -1px 0 #fff,
-1px 1px 0 #fff,
1px 1px 0 #fff;
}
/* - Icon in a Square ------------------------------------------------------- */
.phui-icon-view.phui-icon-square {
height: 40px;
width: 40px;
color: #fff;
font-size: 26px;
text-align: center;
line-height: 38px;
border-radius: 3px;
}
a.phui-icon-view.phui-icon-square:hover {
text-decoration: none;
color: #fff;
}
+
+
+.phui-icon-emblem {
+ border-radius: 4px;
+}
+
+.phui-timeline-extra .phui-icon-emblem {
+ padding: 4px 6px;
+}
+
+.phui-icon-emblem-violet {
+ background-color: {$violet};
+}
+
+.phui-icon-emblem-red {
+ background-color: {$red};
+}
+
+.phui-icon-emblem-pink {
+ background-color: {$pink};
+}
diff --git a/webroot/rsrc/css/phui/phui-object-box.css b/webroot/rsrc/css/phui/phui-object-box.css
index 4999a4c2c..f95e36eed 100644
--- a/webroot/rsrc/css/phui/phui-object-box.css
+++ b/webroot/rsrc/css/phui/phui-object-box.css
@@ -1,160 +1,165 @@
/**
* @provides phui-object-box-css
*/
.phui-object-box {
position: relative;
padding: 12px 12px 4px 12px;
}
.phui-object-box.phui-object-box-collapsed {
padding: 12px 0 0 0;
}
.device-phone .phui-object-box.phui-object-box-collapsed {
padding: 8px 0 0 0;
}
.phui-object-box.phui-object-box-collapsed .phui-header-shell {
padding: 0 8px 12px 16px;
}
.device-phone .phui-object-box.phui-object-box-collapsed .phui-header-shell {
padding: 0 8px 8px;
}
div.phui-object-box.phui-object-box-flush {
margin-top: 0;
}
.phui-object-box .phui-header-shell + .phui-info-view {
margin: 12px 0 0 0;
}
.phui-object-box.phui-object-box-collapsed
.phui-header-shell + .phui-info-view {
margin: 0;
border-radius: 0;
border: 0;
border-bottom: 1px solid {$thinblueborder};
}
.device-phone .phui-object-box {
margin: 8px;
padding: 8px 8px 4px 8px;
}
.device-phone .phui-object-box .phui-header-shell {
padding: 4px 0 12px 4px;
}
.device-tablet .phui-object-box {
margin: 8px 8px 0 8px;
}
.phui-object-box .phui-header-header .phui-tag-view {
margin-left: 8px;
}
.phui-object-box .phui-header-header .phui-tag-core {
border-color: transparent;
padding: 1px 6px;
font-size: {$normalfontsize};
}
/* - Object Box Colors ------------------------------------------------------ */
.phui-box-border.phui-object-box-green {
border: 1px solid {$green};
}
.phui-box-border.phui-object-box-green .phui-header-view {
color: {$green};
}
.phui-box-border.phui-object-box-green .phui-header-shell {
border-bottom-color: {$lightgreen};
}
.phui-box-border.phui-object-box-blue {
border: 1px solid {$blue};
}
.phui-box-border.phui-object-box-blue .phui-header-view {
color: {$blue};
}
.phui-box-border.phui-object-box-blue .phui-header-shell {
border-bottom-color: {$lightblue};
}
.phui-box-border.phui-object-box-red {
border: 1px solid {$red};
}
.phui-box-border.phui-object-box-red .phui-header-view {
color: {$red};
}
.phui-box-border.phui-object-box-red .phui-header-shell {
border-bottom-color: {$lightred};
}
.phui-object-box-hidden-content {
background: {$lightgreybackground};
border-bottom: 1px solid {$thinblueborder};
}
.phui-object-box.phui-object-box-collapsed .phui-object-box-hidden-content {
margin: 0;
}
/* - Double Object Box Override --------------------------------------------- */
.phui-object-box .phui-object-box {
padding: 0;
}
/* eh oh el */
.phui-object-box .phui-object-box + .phui-object-box {
border-top: 1px solid {$thinblueborder};
}
.phui-object-box .phui-object-box .phui-header-shell .phui-header-header {
font-family: {$fontfamily};
}
.phui-object-box .phui-box-border {
border-width: 0;
padding: 0;
margin: 0;
}
.phui-object-box .phui-box-border.phui-box-blue-property {
border-width: 1px;
}
.phui-object-box .phui-object-box .phui-header-shell .phui-header-header {
font-size: {$normalfontsize};
margin: 0;
color: {$darkbluetext};
font-weight: bold;
}
.phui-object-box .phui-object-box .phui-header-shell {
margin: 0;
padding: 4px 8px;
background-color: {$lightgreybackground};
}
/* - Pager at the bottom ---------------------------------------------------- */
.phui-object-box-pager {
background-color: {$bluebackground};
border-top: 1px solid {$lightblueborder};
}
.phui-object-box-pager a.button {
margin-top: 8px;
margin-bottom: 8px;
}
+
+.phui-object-box-instructions {
+ padding: 16px;
+ border-bottom: 1px solid {$thinblueborder};
+}
diff --git a/webroot/rsrc/css/phui/phui-tag-view.css b/webroot/rsrc/css/phui/phui-tag-view.css
index 73675a44d..57529645a 100644
--- a/webroot/rsrc/css/phui/phui-tag-view.css
+++ b/webroot/rsrc/css/phui/phui-tag-view.css
@@ -1,519 +1,527 @@
/**
* @provides phui-tag-view-css
*/
.phui-tag-view {
font-weight: bold;
text-decoration: none;
position: relative;
-webkit-font-smoothing: antialiased;
white-space: nowrap;
}
a.phui-tag-view:hover {
text-decoration: none;
}
.phui-tag-core-closed {
text-decoration: line-through;
color: rgba({$alphablack},0.5);
}
.phui-tag-core-closed:hover {
text-decoration: none;
}
.phui-tag-core {
color: inherit;
border: 1px solid transparent;
border-radius: 3px;
padding: 0 4px;
}
.phui-tag-type-state .phui-tag-core {
padding: 1px 6px;
}
.phui-tag-view.phui-tag-type-state .phui-icon-view {
margin: 0 6px 0 0;
}
.phui-tag-view .phui-icon-view {
display: inline-block;
margin: 0 4px 0 2px;
}
.phui-tag-dot {
position: relative;
display: inline-block;
width: 5px;
height: 5px;
margin-right: 4px;
top: -1px;
border-radius: 5px;
border: 1px solid transparent;
}
+.tokenizer-result .phui-tag-dot {
+ margin-right: 6px;
+}
+
+.jx-tokenizer-token .phui-tag-dot {
+ margin-left: 2px;
+}
+
.phui-tag-type-state {
color: #ffffff;
text-shadow: rgba(100, 100, 100, 0.40) 0px -1px 1px;
}
.phui-tag-type-object,
a.phui-tag-type-object,
a.phui-tag-type-object:link,
.phui-tag-core-closed .phui-tag-color-object {
color: {$blacktext};
}
.phui-tag-type-person {
white-space: nowrap;
color: {$anchor};
}
.phui-tag-color-red {
background-color: {$red};
border-color: {$red};
}
.phui-tag-color-orange {
background-color: {$orange};
border-color: {$orange};
}
.phui-tag-color-yellow {
background-color: {$yellow};
border-color: {$yellow};
}
.phui-tag-color-blue {
background-color: {$blue};
border-color: {$blue};
}
.phui-tag-color-indigo {
background-color: {$indigo};
border-color: {$indigo};
}
.phui-tag-color-green {
background-color: {$green};
border-color: {$green};
}
.phui-tag-color-violet {
background-color: {$violet};
border-color: {$violet};
}
.phui-tag-color-black {
background-color: {$darkgreybackground};
border-color: {$darkgreybackground};
}
.phui-tag-color-grey {
background-color: {$lightgreytext};
border-color: {$lightgreytext};
}
.phui-tag-color-white {
background-color: {$lightgreybackground};
border-color: {$lightgreybackground};
}
.phui-tag-color-object {
background-color: {$greybackground};
border-color: {$lightgreyborder};
}
.phui-tag-color-person {
background-color: {$bluebackground};
border-color: {$thinblueborder};
}
a.phui-tag-view:hover
.phui-tag-core.phui-tag-color-person {
border-color: {$lightblueborder};
}
a.phui-tag-view:hover
.phui-tag-core.phui-tag-color-object {
border-color: {$greyborder};
}
.phabricator-handle-tag-list-item + .phabricator-handle-tag-list-item {
margin-top: 4px;
}
.phui-oi .phabricator-handle-tag-list {
display: inline;
}
.phui-oi .phabricator-handle-tag-list-item {
display: inline-block;
margin: 0 4px 2px 0;
}
.phui-tag-view.phui-tag-border-none .phui-tag-core {
border-color: transparent;
}
a.phui-tag-view:hover.phui-tag-border-none .phui-tag-core {
border-color: transparent !important;
text-decoration: underline;
}
/* - Shaded Tags ---------------------------------------------------------------
For object representation inside text areas and lists.
*/
.phui-tag-view.phui-tag-type-shade {
font-weight: normal;
}
.phui-tag-view.phui-tag-type-shade .phui-icon-view {
font-size: 12px;
}
/* - Slim Tags -----------------------------------------------------------------
A thinner tag for object list, workboards.
*/
.phui-tag-slim .phui-icon-view {
font-size: 11px;
}
.phui-tag-slim .phui-tag-core {
font-size: {$smallerfontsize};
}
/* - Red -------------------------------------------------------------------- */
.phui-tag-red .phui-tag-core,
.jx-tokenizer-token.red {
background: {$sh-redbackground};
border-color: {$sh-lightredborder};
color: {$sh-redtext};
}
.phui-tag-red .phui-icon-view,
.jx-tokenizer-token.red .phui-icon-view,
.jx-tokenizer-token.red .jx-tokenizer-x {
color: {$sh-redicon};
}
a.phui-tag-view:hover.phui-tag-red .phui-tag-core,
.jx-tokenizer-token.red:hover {
border-color: {$sh-redborder};
}
/* - Orange ----------------------------------------------------------------- */
.phui-tag-orange .phui-tag-core,
.jx-tokenizer-token.orange {
background: {$sh-orangebackground};
border-color: {$sh-lightorangeborder};
color: {$sh-orangetext};
}
.phui-tag-orange .phui-icon-view,
.jx-tokenizer-token.orange .phui-icon-view,
.jx-tokenizer-token.orange .jx-tokenizer-x {
color: {$sh-orangeicon};
}
a.phui-tag-view:hover.phui-tag-orange .phui-tag-core,
.jx-tokenizer-token.orange:hover {
border-color: {$sh-orangeborder};
}
/* - Yellow ----------------------------------------------------------------- */
.phui-tag-yellow .phui-tag-core,
.jx-tokenizer-token.yellow {
background: {$sh-yellowbackground};
border-color: {$sh-lightyellowborder};
color: {$sh-yellowtext};
}
.phui-tag-yellow .phui-icon-view,
.jx-tokenizer-token.yellow .phui-icon-view,
.jx-tokenizer-token.yellow .jx-tokenizer-x {
color: {$sh-yellowicon};
}
a.phui-tag-view:hover.phui-tag-yellow .phui-tag-core,
.jx-tokenizer-token.yellow:hover {
border-color: {$sh-yellowborder};
}
/* - Blue ------------------------------------------------------------------- */
.phui-tag-blue .phui-tag-core,
.jx-tokenizer-token.blue {
background: {$sh-bluebackground};
border-color: {$sh-lightblueborder};
color: {$sh-bluetext};
}
.phui-tag-blue .phui-icon-view,
.jx-tokenizer-token.blue .phui-icon-view,
.jx-tokenizer-token.blue .jx-tokenizer-x {
color: {$sh-blueicon};
}
a.phui-tag-view:hover.phui-tag-blue .phui-tag-core,
.jx-tokenizer-token.blue:hover {
border-color: {$sh-blueborder};
}
/* - Sky ------------------------------------------------------------------- */
.phui-tag-sky .phui-tag-core,
.jx-tokenizer-token.sky {
background: #E0F0FA;
border-color: {$sh-lightblueborder};
color: {$sh-bluetext};
}
.phui-tag-sky .phui-icon-view,
.jx-tokenizer-token.sky .phui-icon-view,
.jx-tokenizer-token.sky .jx-tokenizer-x {
color: {$sh-blueicon};
}
a.phui-tag-view:hover.phui-tag-sky .phui-tag-core,
.jx-tokenizer-token.sky:hover {
border-color: {$sh-blueborder};
}
/* - Indigo ----------------------------------------------------------------- */
.phui-tag-indigo .phui-tag-core,
.jx-tokenizer-token.indigo {
background: {$sh-indigobackground};
border-color: {$sh-lightindigoborder};
color: {$sh-indigotext};
}
.phui-tag-indigo .phui-icon-view,
.jx-tokenizer-token.indigo .phui-icon-view,
.jx-tokenizer-token.indigo .jx-tokenizer-x {
color: {$sh-indigoicon};
}
a.phui-tag-view:hover.phui-tag-indigo .phui-tag-core,
.jx-tokenizer-token.indigo:hover {
border-color: {$sh-indigoborder};
}
/* - Green ------------------------------------------------------------------ */
.phui-tag-green .phui-tag-core,
.jx-tokenizer-token.green {
background: {$sh-greenbackground};
border-color: {$sh-lightgreenborder};
color: {$sh-greentext};
}
.phui-tag-green .phui-icon-view,
.jx-tokenizer-token.green .phui-icon-view,
.jx-tokenizer-token.green .jx-tokenizer-x {
color: {$sh-greenicon};
}
a.phui-tag-view:hover.phui-tag-green .phui-tag-core,
.jx-tokenizer-token.green:hover {
border-color: {$sh-greenborder};
}
/* - Violet ----------------------------------------------------------------- */
.phui-tag-violet .phui-tag-core,
.jx-tokenizer-token.violet {
background: {$sh-violetbackground};
border-color: {$sh-lightvioletborder};
color: {$sh-violettext};
}
.phui-tag-violet .phui-icon-view,
.jx-tokenizer-token.violet .phui-icon-view,
.jx-tokenizer-token.violet .jx-tokenizer-x {
color: {$sh-violeticon};
}
a.phui-tag-view:hover.phui-tag-violet .phui-tag-core,
.jx-tokenizer-token.violet:hover {
border-color: {$sh-violetborder};
}
/* - Pink ------------------------------------------------------------------- */
.phui-tag-pink .phui-tag-core,
.jx-tokenizer-token.pink {
background: {$sh-pinkbackground};
border-color: {$sh-lightpinkborder};
color: {$sh-pinktext};
}
.phui-tag-pink .phui-icon-view,
.jx-tokenizer-token.pink .phui-icon-view,
.jx-tokenizer-token.pink .jx-tokenizer-x {
color: {$sh-pinkicon};
}
a.phui-tag-view:hover.phui-tag-pink .phui-tag-core,
.jx-tokenizer-token.pink:hover {
border-color: {$sh-pinkborder};
}
/* - Grey ------------------------------------------------------------------- */
.phui-tag-grey .phui-tag-core,
.jx-tokenizer-token.grey {
background: {$sh-greybackground};
border-color: {$sh-lightgreyborder};
color: {$sh-greytext};
}
.phui-tag-grey .phui-icon-view,
.jx-tokenizer-token.grey .phui-icon-view,
.jx-tokenizer-token.grey .jx-tokenizer-x {
color: {$sh-greyicon};
}
a.phui-tag-view:hover.phui-tag-grey .phui-tag-core,
.jx-tokenizer-token.grey:hover {
border-color: {$sh-greyborder};
}
/* - Checkered -------------------------------------------------------------- */
.phui-tag-checkered .phui-tag-core,
.jx-tokenizer-token.checkered {
background: url(/rsrc/image/checker_lighter.png);
border-style: dashed;
border-color: {$sh-greyborder};
color: {$sh-greytext};
text-shadow: 1px 1px #fff;
}
.phui-tag-checkered .phui-icon-view,
.jx-tokenizer-token.checkered .phui-icon-view,
.jx-tokenizer-token.checkered .jx-tokenizer-x {
color: {$sh-greyicon};
}
a.phui-tag-view:hover.phui-tag-checkered .phui-tag-core,
.jx-tokenizer-token.checkered:hover {
border-style: solid;
border-color: {$sh-greyborder};
}
/* - Disabled --------------------------------------------------------------- */
.phui-tag-disabled .phui-tag-core {
background-color: {$sh-disabledbackground};
border-color: {$sh-lightdisabledborder};
color: {$sh-disabledtext};
}
.phui-tag-disabled .phui-icon-view {
color: {$sh-disabledicon};
}
a.phui-tag-view:hover.phui-tag-disabled .phui-tag-core {
border-color: {$sh-disabledborder};
}
/* - Outline Tags --------------------------------------------------------------
Basic Tag with a bold border and white background
*/
.phui-tag-type-outline {
text-transform: uppercase;
font-weight: normal;
}
.phui-tag-view.phui-tag-type-outline .phui-tag-core {
background: #fff;
padding: 0 6px 1px 6px;
}
.phui-tag-slim.phui-tag-type-outline .phui-tag-core {
font-size: {$smallestfontsize};
}
.phui-tag-type-outline.phui-tag-red .phui-tag-core {
color: {$red};
border-color: {$red};
}
.phui-tag-type-outline.phui-tag-orange .phui-tag-core {
color: {$orange};
border-color: {$orange};
}
.phui-tag-type-outline.phui-tag-yellow .phui-tag-core {
color: {$yellow};
border-color: {$yellow};
}
.phui-tag-type-outline.phui-tag-green .phui-tag-core {
color: {$green};
border-color: {$green};
}
.phui-tag-type-outline.phui-tag-blue .phui-tag-core {
color: {$blue};
border-color: {$blue};
}
.phui-tag-type-outline.phui-tag-indigo .phui-tag-core {
color: {$indigo};
border-color: {$indigo};
}
.phui-tag-type-outline.phui-tag-violet .phui-tag-core {
color: {$violet};
border-color: {$violet};
}
.phui-tag-type-outline.phui-tag-grey .phui-tag-core {
color: {$bluetext};
border-color: {$bluetext};
}
.phui-tag-type-outline.phui-tag-disabled .phui-tag-core {
color: {$lightgreytext};
border-color: {$lightgreytext};
}
.phui-tag-type-outline.phui-tag-pink .phui-tag-core {
color: {$pink};
border-color: {$pink};
}
.phui-tag-type-outline.phui-tag-sky .phui-tag-core {
color: {$sky};
border-color: {$sky};
}
.phui-tag-type-outline.phui-tag-fire .phui-tag-core {
color: {$fire};
border-color: {$fire};
}
.phui-tag-type-outline.phui-tag-black .phui-tag-core {
color: {$blacktext};
border-color: {$blacktext};
}
diff --git a/webroot/rsrc/css/phui/workboards/phui-workcard.css b/webroot/rsrc/css/phui/workboards/phui-workcard.css
index e137e962b..3c6a798fc 100644
--- a/webroot/rsrc/css/phui/workboards/phui-workcard.css
+++ b/webroot/rsrc/css/phui/workboards/phui-workcard.css
@@ -1,176 +1,195 @@
/**
* @provides phui-workcard-view-css
*/
.phui-workcard.phui-oi {
background-color: {$page.content};
border-radius: 3px;
margin-bottom: 8px;
border-left-width: 4px;
box-sizing: border-box;
}
.phui-workcard .phui-oi-name {
padding-bottom: 4px;
}
.phui-workcard .phui-oi-content {
margin-top: 0;
}
.phui-workcard .phui-oi-frame {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
border-color: {$thinblueborder};
border-bottom-color: {$lightblueborder};
}
.phui-workcard.phui-oi .phui-oi-objname {
-webkit-touch-callout: text;
-webkit-user-select: text;
-khtml-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
}
.phui-workcard .phui-oi-link {
white-space: normal;
font-weight: normal;
color: {$blacktext};
margin-left: 2px;
}
.phui-oi-disabled.phui-workcard {
background-color: rgba({$alphawhite},.67);
}
.phui-oi-disabled.phui-workcard .phui-oi-link {
color: {$greytext};
}
.device-desktop .phui-workcard .phui-oi-with-1-actions
.phui-oi-content-box {
margin-right: 0;
overflow: hidden;
}
.phui-workcard .phui-oi-objname {
vertical-align: top;
}
-.phui-workcard.phui-oi-grippable .phui-oi-frame {
- padding-left: 0;
-}
-
-.phui-workcard .phui-oi-grip {
- display: none;
-}
-
.device-desktop .phui-workcard .phui-list-item-icon {
display: none;
}
.phui-workcard.phui-oi .phui-list-item-href {
height: 24px;
width: 24px;
}
.device-desktop .phui-workcard.phui-oi:hover
.phui-list-item-href {
background: #fff;
opacity: .7;
}
.device-desktop .phui-workcard.phui-oi
.phui-list-item-href:hover {
background: {$sh-bluebackground};
opacity: 1;
}
+.device-desktop .phui-workcard.draggable-card {
+ cursor: grab;
+}
+
+.jx-dragging .phui-workcard.draggable-card {
+ cursor: grabbing;
+}
+
+.device-desktop .phui-workcard.undraggable-card {
+ cursor: not-allowed;
+}
+
+.device-desktop .phui-workcard.phui-oi.not-editable:hover {
+ background: {$sh-redbackground};
+}
+
+.device-desktop .phui-workcard.phui-oi.not-editable:hover
+ .phui-list-item-href {
+ border-radius: 3px;
+ background: {$red};
+}
+
+.device-desktop .phui-workcard.phui-oi.not-editable:hover
+ .phui-list-item-href .phui-icon-view {
+ color: #fff;
+}
+
.phui-workcard.phui-oi:hover .phui-list-item-icon {
display: block;
}
.phui-workcard .phui-oi-attributes {
margin-right: 12px;
}
.phui-workpanel-view .drag-ghost {
margin-bottom: 8px;
}
.phui-workcard .phui-oi-cover-image {
display: block;
padding: 8px 8px 0 8px;
width: 263px;
}
.phui-workcard.phui-oi.phui-workcard-upload-target {
background-color: {$sh-greenbackground};
}
.phui-oi-list-view .phui-workcard:last-child {
margin-bottom: 0;
}
.phui-workcard .phui-oi-attribute-spacer {
display: none;
}
.phui-workcard .phui-workcard-points {
margin: 0 4px 2px 0;
display: inline-block;
}
.phui-workcard .phui-oi-attribute {
display: inline;
}
/* - Draggable Colors --------------------------------------------------------*/
.phui-workcard.phui-oi.drag-clone {
box-shadow: {$dropshadow};
background-color: {$sh-greybackground};
}
.phui-workcard.phui-oi.drag-clone .phui-list-item-href {
display: none;
}
.phui-workcard.drag-clone.phui-oi-bar-color-red {
background-color: {$sh-redbackground};
}
.phui-workcard.drag-clone.phui-oi-bar-color-orange {
background-color: {$sh-orangebackground};
}
.phui-workcard.drag-clone.phui-oi-bar-color-yellow {
background-color: {$sh-yellowbackground};
}
.phui-workcard.drag-clone.phui-oi-bar-color-green {
background-color: {$sh-greenbackground};
}
.phui-workcard.drag-clone.phui-oi-bar-color-blue {
background-color: {$sh-bluebackground};
}
.phui-workcard.drag-clone.phui-oi-bar-color-indigo {
background-color: {$sh-indigobackground};
}
.phui-workcard.drag-clone.phui-oi-bar-color-violet {
background-color: {$sh-violetbackground};
}
.phui-workcard.drag-clone.phui-oi-bar-color-pink {
background-color: {$sh-pinkbackground};
}
.phui-workcard.drag-clone.phui-oi-bar-color-sky {
background-color: {$sh-bluebackground};
}
diff --git a/webroot/rsrc/css/phui/workboards/phui-workpanel.css b/webroot/rsrc/css/phui/workboards/phui-workpanel.css
index 617ff5aa6..5ee54f2de 100644
--- a/webroot/rsrc/css/phui/workboards/phui-workpanel.css
+++ b/webroot/rsrc/css/phui/workboards/phui-workpanel.css
@@ -1,139 +1,254 @@
/**
* @provides phui-workpanel-view-css
* @requires phui-workcard-view-css
*/
.phui-workpanel-view .phui-header-shell {
padding: 8px;
width: 284px;
}
.phui-workpanel-view .phui-header-shell .phui-header-header {
font-size: {$biggerfontsize};
line-height: 18px;
color: {$darkbluetext};
}
.phui-workpanel-view .phui-header-shell .phui-header-subheader {
padding: 0 4px;
margin: 0;
display: inline-block;
color: {$lightgreytext};
font-size: {$normalfontsize};
}
.device .phui-workpanel-view .phui-header-shell {
width: auto;
}
.phui-workboard-view {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.phui-workpanel-view .phui-box-grey {
background-color: rgba({$alphablue},0.1);
}
.phui-workpanel-view.phui-workboard-column-milestone .phui-box-grey {
background-color: rgba(234, 230, 247, 0.85);
}
.phui-workpanel-view .phui-header-col2 .phui-icon-view {
margin-right: 4px;
}
.phui-workpanel-view .phui-workpanel-header-action {
float: right;
width: 24px;
}
.phui-workpanel-view .phui-workpanel-body {
padding: 8px 4px 8px 0;
}
.phui-workpanel-view .phui-workpanel-body-content {
padding: 0 4px 0 8px;
}
.device .phui-workpanel-view .phui-workpanel-body {
padding: 8px 0;
}
.phui-workpanel-view .phui-workpanel-footer-action a {
color: {$darkbluetext};
font-weight: bold;
}
.device-desktop .phui-workpanel-view .phui-workpanel-footer-action:hover {
background-color: rgba(100,100,100,.1);
border-radius: 3px;
}
.device-desktop .aphront-multi-column-fixed .phui-workpanel-view {
width: 300px;
}
.device-phone .aphront-multi-column-fixed .phui-workpanel-view,
.device-phone .phui-workpanel-view .phui-header-shell {
width: auto;
}
.phui-workpanel-body .phui-oi-list-view {
min-height: 54px;
background-color: transparent;
}
.device .aphront-multi-column-outer
div.aphront-multi-column-column-outer .phui-workpanel-body {
width: auto;
}
.project-panel-hidden {
opacity: 0.75;
}
.device-desktop .phui-workpanel-body-content {
max-height: calc(100vh - 162px);
overflow-y: auto;
overflow-x: hidden;
}
.device-desktop .phui-workpanel-body-content::-webkit-scrollbar {
height: 8px;
width: 8px;
background: rgba({$alphablue},0.2);
border-radius: 4px;
}
.device-desktop .phui-workpanel-body-content::-webkit-scrollbar-thumb {
background: rgba({$alphablue},0.4);
border-radius: 4px;
}
.project-panel-empty .phui-oi-list-view {
background: rgba(234, 230, 247, 0.85);
border-radius: 3px;
margin-bottom: 4px;
border: 1px dashed {$sh-indigoborder};
}
.project-panel-empty .phui-oi-list-view .drag-ghost {
display: none;
}
.project-panel-empty .phui-oi-list-view.drag-target-list {
background: rgba({$alphawhite},.7);
}
.phui-workpanel-view.project-panel-over-limit .phui-header-header {
color: {$red};
}
.phui-workpanel-view.project-panel-over-limit .phui-header-shell {
border-color: {$red};
}
+
+.phui-workpanel-view .phui-box-grey {
+ border: 1px solid transparent;
+}
+
+.phui-workpanel-view.workboard-column-drop-target .phui-box-grey {
+ border-color: {$lightblueborder};
+}
+
+.workboard-group-header {
+ background: rgba({$alphablue}, 0.10);
+ padding: 6px 8px;
+ margin: 0 0 8px -8px;
+ border-bottom: 1px solid {$lightgreyborder};
+ font-weight: bold;
+ color: {$darkgreytext};
+ position: relative;
+}
+
+.workboard-group-header .phui-icon-view {
+ position: absolute;
+ display: inline-block;
+ width: 24px;
+ padding: 5px 0 0 0;
+ height: 19px;
+ background-size: 100%;
+ border-radius: 3px;
+ background-repeat: no-repeat;
+ text-align: center;
+ background-color: {$lightgreybackground};
+ border: 1px solid {$lightgreybackground};
+}
+
+.workboard-group-header .workboard-group-header-name {
+ display: block;
+ position: relative;
+ height: 24px;
+ line-height: 24px;
+ margin-left: 36px;
+ overflow: hidden;
+}
+
+.workboard-drop-preview {
+ pointer-events: none;
+ position: absolute;
+ bottom: 12px;
+ right: 12px;
+ width: 300px;
+ border-radius: 3px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
+ border: 1px solid {$lightblueborder};
+ padding: 4px 0;
+ background: #fff;
+}
+
+.workboard-drop-preview li {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin: 4px 8px;
+ color: {$greytext};
+ border-radius: 3px;
+}
+
+.workboard-drop-preview li .phui-icon-view {
+ position: relative;
+ display: inline-block;
+ text-align: center;
+ width: 24px;
+ height: 18px;
+ padding-top: 6px;
+ border-radius: 3px;
+ background: {$bluebackground};
+ margin-right: 6px;
+}
+
+.workboard-drop-preview .workboard-drop-preview-header {
+ background: {$sky};
+ color: #fff;
+}
+
+.workboard-drop-preview .workboard-drop-preview-header .phui-icon-view {
+ background: {$blue};
+ color: #fff;
+}
+
+.workboard-drop-preview-fade {
+ animation: 0.1s workboard-drop-preview-fade-out;
+ opacity: 0.25;
+}
+
+@keyframes workboard-drop-preview-fade-out {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0.25;
+ }
+}
+
+.phui-workpanel-view .phui-header-action-item a.phui-icon-view {
+ width: 24px;
+ height: 24px;
+ line-height: 24px;
+ text-align: center;
+ border-radius: 3px;
+ box-shadow: inset -1px -1px 2px rgba(0, 0, 0, 0.05);
+ border: 1px solid {$lightgreyborder};
+ background: {$lightgreybackground};
+}
+
+.phui-workpanel-view .phui-header-action-item .phui-tag-view {
+ line-height: 24px;
+}
diff --git a/webroot/rsrc/externals/javelin/lib/Sound.js b/webroot/rsrc/externals/javelin/lib/Sound.js
index accbe3d29..68181560f 100644
--- a/webroot/rsrc/externals/javelin/lib/Sound.js
+++ b/webroot/rsrc/externals/javelin/lib/Sound.js
@@ -1,38 +1,82 @@
/**
* @requires javelin-install
* @provides javelin-sound
* @javelin
*/
JX.install('Sound', {
statics: {
_sounds: {},
+ _queue: [],
+ _playingQueue: false,
load: function(uri) {
var self = JX.Sound;
if (!(uri in self._sounds)) {
- self._sounds[uri] = JX.$N(
+ var audio = JX.$N(
'audio',
{
src: uri,
preload: 'auto'
});
+
+ // In Safari, it isn't good enough to just load a sound in response
+ // to a click: we must also play it. Once we've played it once, we
+ // can continue to play it freely.
+
+ // Play the sound, then immediately pause it. This rejects the "play()"
+ // promise but marks the audio as playable, so our "play()" method will
+ // work correctly later.
+ if (window.webkitAudioContext) {
+ audio.play().then(JX.bag, JX.bag);
+ audio.pause();
+ }
+
+ self._sounds[uri] = audio;
}
},
- play: function(uri) {
+ play: function(uri, callback) {
var self = JX.Sound;
self.load(uri);
var sound = self._sounds[uri];
try {
- sound.play();
+ sound.onended = callback || JX.bag;
+ sound.play().then(JX.bag, callback || JX.bag);
} catch (ex) {
JX.log(ex);
}
+ },
+
+ queue: function(uri) {
+ var self = JX.Sound;
+ self._queue.push(uri);
+ self._playQueue();
+ },
+
+ _playQueue: function() {
+ var self = JX.Sound;
+ if (self._playingQueue) {
+ return;
+ }
+ self._playingQueue = true;
+ self._nextQueue();
+ },
+
+ _nextQueue: function() {
+ var self = JX.Sound;
+ if (self._queue.length) {
+ var next = self._queue[0];
+ self._queue.splice(0, 1);
+ self.play(next, self._nextQueue);
+ } else {
+ self._playingQueue = false;
+ }
}
+
}
});
diff --git a/webroot/rsrc/image/chevron-in.png b/webroot/rsrc/image/chevron-in.png
new file mode 100644
index 000000000..373d39cfe
Binary files /dev/null and b/webroot/rsrc/image/chevron-in.png differ
diff --git a/webroot/rsrc/image/chevron-out.png b/webroot/rsrc/image/chevron-out.png
new file mode 100644
index 000000000..787772eb2
Binary files /dev/null and b/webroot/rsrc/image/chevron-out.png differ
diff --git a/webroot/rsrc/js/application/diff/DiffChangeset.js b/webroot/rsrc/js/application/diff/DiffChangeset.js
index 24d734573..754f3b16e 100644
--- a/webroot/rsrc/js/application/diff/DiffChangeset.js
+++ b/webroot/rsrc/js/application/diff/DiffChangeset.js
@@ -1,891 +1,888 @@
/**
* @provides phabricator-diff-changeset
* @requires javelin-dom
* javelin-util
* javelin-stratcom
* javelin-install
* javelin-workflow
* javelin-router
* javelin-behavior-device
* javelin-vector
* phabricator-diff-inline
* @javelin
*/
JX.install('DiffChangeset', {
construct : function(node) {
this._node = node;
var data = this._getNodeData();
this._renderURI = data.renderURI;
this._ref = data.ref;
- this._whitespace = data.whitespace;
this._renderer = data.renderer;
this._highlight = data.highlight;
this._encoding = data.encoding;
this._loaded = data.loaded;
this._treeNodeID = data.treeNodeID;
this._leftID = data.left;
this._rightID = data.right;
this._displayPath = JX.$H(data.displayPath);
this._icon = data.icon;
this._inlines = [];
},
members: {
_node: null,
_loaded: false,
_sequence: 0,
_stabilize: false,
_renderURI: null,
_ref: null,
- _whitespace: null,
_renderer: null,
_highlight: null,
_encoding: null,
_undoTemplates: null,
_leftID: null,
_rightID: null,
_inlines: null,
_visible: true,
_undoNode: null,
_displayPath: null,
_changesetList: null,
_icon: null,
_treeNodeID: null,
getLeftChangesetID: function() {
return this._leftID;
},
getRightChangesetID: function() {
return this._rightID;
},
setChangesetList: function(list) {
this._changesetList = list;
return this;
},
getIcon: function() {
if (!this._visible) {
return 'fa-file-o';
}
return this._icon;
},
getColor: function() {
if (!this._visible) {
return 'grey';
}
return 'blue';
},
getChangesetList: function() {
return this._changesetList;
},
/**
* Has the content of this changeset been loaded?
*
* This method returns `true` if a request has been fired, even if the
* response has not returned yet.
*
* @return bool True if the content has been loaded.
*/
isLoaded: function() {
return this._loaded;
},
/**
* Configure stabilization of the document position on content load.
*
* When we dump the changeset into the document, we can try to stabilize
* the document scroll position so that the user doesn't feel like they
* are jumping around as things load in. This is generally useful when
* populating initial changes.
*
* However, if a user explicitly requests a content load by clicking a
* "Load" link or using the dropdown menu, this stabilization generally
* feels unnatural, so we don't use it in response to explicit user action.
*
* @param bool True to stabilize the next content fill.
* @return this
*/
setStabilize: function(stabilize) {
this._stabilize = stabilize;
return this;
},
/**
* Should this changeset load immediately when the page loads?
*
* Normally, changes load immediately, but if a diff or commit is very
* large we stop doing this and have the user load files explicitly, or
* choose to load everything.
*
* @return bool True if the changeset should load automatically when the
* page loads.
*/
shouldAutoload: function() {
return this._getNodeData().autoload;
},
/**
* Load this changeset, if it isn't already loading.
*
* This fires a request to fill the content of this changeset, provided
* there isn't already a request in flight. To force a reload, use
* @{method:reload}.
*
* @return this
*/
load: function() {
if (this._loaded) {
return this;
}
return this.reload();
},
/**
* Reload the changeset content.
*
* This method always issues a request, even if the content is already
* loading. To load conditionally, use @{method:load}.
*
* @return this
*/
reload: function() {
this._loaded = true;
this._sequence++;
var params = this._getViewParameters();
var pht = this.getChangesetList().getTranslations();
var workflow = new JX.Workflow(this._renderURI, params)
.setHandler(JX.bind(this, this._onresponse, this._sequence));
this._startContentWorkflow(workflow);
JX.DOM.setContent(
this._getContentFrame(),
JX.$N(
'div',
{className: 'differential-loading'},
pht('Loading...')));
return this;
},
/**
* Load missing context in a changeset.
*
* We do this when the user clicks "Show X Lines". We also expand all of
* the missing context when they "Show All Context".
*
* @param string Line range specification, like "0-40/0-20".
* @param node Row where the context should be rendered after loading.
* @param bool True if this is a bulk load of multiple context blocks.
* @return this
*/
loadContext: function(range, target, bulk) {
var params = this._getViewParameters();
params.range = range;
var pht = this.getChangesetList().getTranslations();
var container = JX.DOM.scry(target, 'td')[0];
JX.DOM.setContent(container, pht('Loading...'));
JX.DOM.alterClass(target, 'differential-show-more-loading', true);
var workflow = new JX.Workflow(this._renderURI, params)
.setHandler(JX.bind(this, this._oncontext, target));
if (bulk) {
// If we're loading a bunch of these because the viewer clicked
// "Show All Context" or similar, use lower-priority requests
// and draw a progress bar.
this._startContentWorkflow(workflow);
} else {
// If this is a single click on a context link, use a higher priority
// load without a chrome change.
workflow.start();
}
return this;
},
loadAllContext: function() {
var nodes = JX.DOM.scry(this._node, 'tr', 'context-target');
for (var ii = 0; ii < nodes.length; ii++) {
var show = JX.DOM.scry(nodes[ii], 'a', 'show-more');
for (var jj = 0; jj < show.length; jj++) {
var data = JX.Stratcom.getData(show[jj]);
if (data.type != 'all') {
continue;
}
this.loadContext(data.range, nodes[ii], true);
}
}
},
_startContentWorkflow: function(workflow) {
var routable = workflow.getRoutable();
routable
.setPriority(500)
.setType('content')
.setKey(this._getRoutableKey());
JX.Router.getInstance().queue(routable);
},
getDisplayPath: function() {
return this._displayPath;
},
/**
* Receive a response to a context request.
*/
_oncontext: function(target, response) {
// TODO: This should be better structured.
// If the response comes back with several top-level nodes, the last one
// is the actual context; the others are headers. Add any headers first,
// then copy the new rows into the document.
var markup = JX.$H(response.changeset).getFragment();
var len = markup.childNodes.length;
var diff = JX.DOM.findAbove(target, 'table', 'differential-diff');
for (var ii = 0; ii < len - 1; ii++) {
diff.parentNode.insertBefore(markup.firstChild, diff);
}
var table = markup.firstChild;
var root = target.parentNode;
this._moveRows(table, root, target);
root.removeChild(target);
this._onchangesetresponse(response);
},
_moveRows: function(src, dst, before) {
var rows = JX.DOM.scry(src, 'tr');
for (var ii = 0; ii < rows.length; ii++) {
// Find the table this <tr /> belongs to. If it's a sub-table, like a
// table in an inline comment, don't copy it.
if (JX.DOM.findAbove(rows[ii], 'table') !== src) {
continue;
}
if (before) {
dst.insertBefore(rows[ii], before);
} else {
dst.appendChild(rows[ii]);
}
}
},
/**
* Get parameters which define the current rendering options.
*/
_getViewParameters: function() {
return {
ref: this._ref,
- whitespace: this._whitespace || '',
renderer: this.getRenderer() || '',
highlight: this._highlight || '',
encoding: this._encoding || ''
};
},
/**
* Get the active @{class:JX.Routable} for this changeset.
*
* After issuing a request with @{method:load} or @{method:reload}, you
* can adjust routable settings (like priority) by querying the routable
* with this method. Note that there may not be a current routable.
*
* @return JX.Routable|null Active routable, if one exists.
*/
getRoutable: function() {
return JX.Router.getInstance().getRoutableByKey(this._getRoutableKey());
},
setRenderer: function(renderer) {
this._renderer = renderer;
return this;
},
getRenderer: function() {
if (this._renderer !== null) {
return this._renderer;
}
// NOTE: If you load the page at one device resolution and then resize to
// a different one we don't re-render the diffs, because it's a
// complicated mess and you could lose inline comments, cursor positions,
// etc.
return (JX.Device.getDevice() == 'desktop') ? '2up' : '1up';
},
getUndoTemplates: function() {
return this._undoTemplates;
},
setEncoding: function(encoding) {
this._encoding = encoding;
return this;
},
getEncoding: function() {
return this._encoding;
},
setHighlight: function(highlight) {
this._highlight = highlight;
return this;
},
getHighlight: function() {
return this._highlight;
},
getSelectableItems: function() {
var items = [];
items.push({
type: 'file',
changeset: this,
target: this,
nodes: {
begin: this._node,
end: null
}
});
if (!this._visible) {
return items;
}
var rows = JX.DOM.scry(this._node, 'tr');
var blocks = [];
var block;
var ii;
for (ii = 0; ii < rows.length; ii++) {
var type = this._getRowType(rows[ii]);
if (!block || (block.type !== type)) {
block = {
type: type,
items: []
};
blocks.push(block);
}
block.items.push(rows[ii]);
}
var last_inline = null;
var last_inline_item = null;
for (ii = 0; ii < blocks.length; ii++) {
block = blocks[ii];
if (block.type == 'change') {
items.push({
type: block.type,
changeset: this,
target: block.items[0],
nodes: {
begin: block.items[0],
end: block.items[block.items.length - 1]
}
});
}
if (block.type == 'comment') {
for (var jj = 0; jj < block.items.length; jj++) {
var inline = this.getInlineForRow(block.items[jj]);
// When comments are being edited, they have a hidden row with
// the actual comment and then a visible row with the editor.
// In this case, we only want to generate one item, but it should
// use the editor as a scroll target. To accomplish this, check if
// this row has the same inline as the previous row. If so, update
// the last item to use this row's nodes.
if (inline === last_inline) {
last_inline_item.nodes.begin = block.items[jj];
last_inline_item.nodes.end = block.items[jj];
continue;
} else {
last_inline = inline;
}
var is_saved = (!inline.isDraft() && !inline.isEditing());
last_inline_item = {
type: block.type,
changeset: this,
target: inline,
hidden: inline.isHidden(),
collapsed: inline.isCollapsed(),
deleted: !inline.getID() && !inline.isEditing(),
nodes: {
begin: block.items[jj],
end: block.items[jj]
},
attributes: {
unsaved: inline.isEditing(),
anyDraft: inline.isDraft() || inline.isDraftDone(),
undone: (is_saved && !inline.isDone()),
done: (is_saved && inline.isDone())
}
};
items.push(last_inline_item);
}
}
}
return items;
},
_getRowType: function(row) {
// NOTE: Don't do "className.indexOf()" elsewhere. This is evil legacy
// magic.
if (row.className.indexOf('inline') !== -1) {
return 'comment';
}
var cells = JX.DOM.scry(row, 'td');
for (var ii = 0; ii < cells.length; ii++) {
if (cells[ii].className.indexOf('old') !== -1 ||
cells[ii].className.indexOf('new') !== -1) {
return 'change';
}
}
},
_getNodeData: function() {
return JX.Stratcom.getData(this._node);
},
getVectors: function() {
return {
pos: JX.$V(this._node),
dim: JX.Vector.getDim(this._node)
};
},
_onresponse: function(sequence, response) {
if (sequence != this._sequence) {
// If this isn't the most recent request, ignore it. This normally
// means the user changed view settings between the time the page loaded
// and the content filled.
return;
}
// As we populate the changeset list, we try to hold the document scroll
// position steady, so that, e.g., users who want to leave a comment on a
// diff with a large number of changes don't constantly have the text
// area scrolled off the bottom of the screen until the entire diff loads.
//
// There are several major cases here:
//
// - If we're near the top of the document, never scroll.
// - If we're near the bottom of the document, always scroll, unless
// we have an anchor.
// - Otherwise, scroll if the changes were above (or, at least,
// almost entirely above) the viewport.
//
// We don't scroll if the changes were just near the top of the viewport
// because this makes us scroll incorrectly when an anchored change is
// visible. See T12779.
var target = this._node;
var old_pos = JX.Vector.getScroll();
var old_view = JX.Vector.getViewport();
var old_dim = JX.Vector.getDocument();
// Number of pixels away from the top or bottom of the document which
// count as "nearby".
var sticky = 480;
var near_top = (old_pos.y <= sticky);
var near_bot = ((old_pos.y + old_view.y) >= (old_dim.y - sticky));
// If we have an anchor in the URL, never stick to the bottom of the
// page. See T11784 for discussion.
if (window.location.hash) {
near_bot = false;
}
var target_pos = JX.Vector.getPos(target);
var target_dim = JX.Vector.getDim(target);
var target_bot = (target_pos.y + target_dim.y);
// Detect if the changeset is entirely (or, at least, almost entirely)
// above us. The height here is roughly the height of the persistent
// banner.
var above_screen = (target_bot < old_pos.y + 64);
// If we have a URL anchor and are currently nearby, stick to it
// no matter what.
var on_target = null;
if (window.location.hash) {
try {
var anchor = JX.$(window.location.hash.replace('#', ''));
if (anchor) {
var anchor_pos = JX.$V(anchor);
if ((anchor_pos.y > old_pos.y) &&
(anchor_pos.y < old_pos.y + 96)) {
on_target = anchor;
}
}
} catch (ignored) {
// If we have a bogus anchor, just ignore it.
}
}
var frame = this._getContentFrame();
JX.DOM.setContent(frame, JX.$H(response.changeset));
if (this._stabilize) {
if (on_target) {
JX.DOM.scrollToPosition(old_pos.x, JX.$V(on_target).y - 60);
} else if (!near_top) {
if (near_bot || above_screen) {
// Figure out how much taller the document got.
var delta = (JX.Vector.getDocument().y - old_dim.y);
JX.DOM.scrollToPosition(old_pos.x, old_pos.y + delta);
}
}
this._stabilize = false;
}
this._onchangesetresponse(response);
},
_onchangesetresponse: function(response) {
// Code shared by autoload and context responses.
if (response.coverage) {
for (var k in response.coverage) {
try {
JX.DOM.replace(JX.$(k), JX.$H(response.coverage[k]));
} catch (ignored) {
// Not terribly important.
}
}
}
if (response.undoTemplates) {
this._undoTemplates = response.undoTemplates;
}
JX.Stratcom.invoke('differential-inline-comment-refresh');
this._rebuildAllInlines();
JX.Stratcom.invoke('resize');
},
_getContentFrame: function() {
return JX.DOM.find(this._node, 'div', 'changeset-view-content');
},
_getRoutableKey: function() {
return 'changeset-view.' + this._ref + '.' + this._sequence;
},
getInlineForRow: function(node) {
var data = JX.Stratcom.getData(node);
if (!data.inline) {
var inline = new JX.DiffInline()
.setChangeset(this)
.bindToRow(node);
this._inlines.push(inline);
}
return data.inline;
},
newInlineForRange: function(origin, target) {
var list = this.getChangesetList();
var src = list.getLineNumberFromHeader(origin);
var dst = list.getLineNumberFromHeader(target);
var changeset_id = null;
var side = list.getDisplaySideFromHeader(origin);
if (side == 'right') {
changeset_id = this.getRightChangesetID();
} else {
changeset_id = this.getLeftChangesetID();
}
var is_new = false;
if (side == 'right') {
is_new = true;
} else if (this.getRightChangesetID() != this.getLeftChangesetID()) {
is_new = true;
}
var data = {
origin: origin,
target: target,
number: src,
length: dst - src,
changesetID: changeset_id,
displaySide: side,
isNewFile: is_new
};
var inline = new JX.DiffInline()
.setChangeset(this)
.bindToRange(data);
this._inlines.push(inline);
inline.create();
return inline;
},
newInlineReply: function(original, text) {
var inline = new JX.DiffInline()
.setChangeset(this)
.bindToReply(original);
this._inlines.push(inline);
inline.create(text);
return inline;
},
getInlineByID: function(id) {
return this._queryInline('id', id);
},
getInlineByPHID: function(phid) {
return this._queryInline('phid', phid);
},
_queryInline: function(field, value) {
// First, look for the inline in the objects we've already built.
var inline = this._findInline(field, value);
if (inline) {
return inline;
}
// If we haven't found a matching inline yet, rebuild all the inlines
// present in the document, then look again.
this._rebuildAllInlines();
return this._findInline(field, value);
},
_findInline: function(field, value) {
for (var ii = 0; ii < this._inlines.length; ii++) {
var inline = this._inlines[ii];
var target;
switch (field) {
case 'id':
target = inline.getID();
break;
case 'phid':
target = inline.getPHID();
break;
}
if (target == value) {
return inline;
}
}
return null;
},
getInlines: function() {
this._rebuildAllInlines();
return this._inlines;
},
_rebuildAllInlines: function() {
var rows = JX.DOM.scry(this._node, 'tr');
var ii;
for (ii = 0; ii < rows.length; ii++) {
var row = rows[ii];
if (this._getRowType(row) != 'comment') {
continue;
}
// As a side effect, this builds any missing inline objects and adds
// them to this Changeset's list of inlines.
this.getInlineForRow(row);
}
},
redrawFileTree: function() {
var tree;
try {
tree = JX.$(this._treeNodeID);
} catch (e) {
return;
}
var inlines = this._inlines;
var done = [];
var undone = [];
var inline;
for (var ii = 0; ii < inlines.length; ii++) {
inline = inlines[ii];
if (inline.isDeleted()) {
continue;
}
if (inline.isSynthetic()) {
continue;
}
if (inline.isEditing()) {
continue;
}
if (!inline.getID()) {
// These are new comments which have been cancelled, and do not
// count as anything.
continue;
}
if (inline.isDraft()) {
continue;
}
if (!inline.isDone()) {
undone.push(inline);
} else {
done.push(inline);
}
}
var total = done.length + undone.length;
var hint;
var is_visible;
var is_completed;
if (total) {
if (done.length) {
hint = [done.length, '/', total];
} else {
hint = total;
}
is_visible = true;
is_completed = (done.length == total);
} else {
hint = '-';
is_visible = false;
is_completed = false;
}
JX.DOM.setContent(tree, hint);
JX.DOM.alterClass(tree, 'filetree-comments-visible', is_visible);
JX.DOM.alterClass(tree, 'filetree-comments-completed', is_completed);
},
toggleVisibility: function() {
this._visible = !this._visible;
var diff = JX.DOM.find(this._node, 'table', 'differential-diff');
var undo = this._getUndoNode();
if (this._visible) {
JX.DOM.show(diff);
JX.DOM.remove(undo);
} else {
JX.DOM.hide(diff);
JX.DOM.appendContent(diff.parentNode, undo);
}
JX.Stratcom.invoke('resize');
},
isVisible: function() {
return this._visible;
},
_getUndoNode: function() {
if (!this._undoNode) {
var pht = this.getChangesetList().getTranslations();
var link_attributes = {
href: '#'
};
var undo_link = JX.$N('a', link_attributes, pht('Show Content'));
var onundo = JX.bind(this, this._onundo);
JX.DOM.listen(undo_link, 'click', null, onundo);
var node_attributes = {
className: 'differential-collapse-undo'
};
var node_content = [
pht('This file content has been collapsed.'),
' ',
undo_link
];
var undo_node = JX.$N('div', node_attributes, node_content);
this._undoNode = undo_node;
}
return this._undoNode;
},
_onundo: function(e) {
e.kill();
this.toggleVisibility();
}
},
statics: {
getForNode: function(node) {
var data = JX.Stratcom.getData(node);
if (!data.changesetViewManager) {
data.changesetViewManager = new JX.DiffChangeset(node);
}
return data.changesetViewManager;
}
}
});
diff --git a/webroot/rsrc/js/application/diff/DiffChangesetList.js b/webroot/rsrc/js/application/diff/DiffChangesetList.js
index 5ba43a7e6..572faad98 100644
--- a/webroot/rsrc/js/application/diff/DiffChangesetList.js
+++ b/webroot/rsrc/js/application/diff/DiffChangesetList.js
@@ -1,1888 +1,1900 @@
/**
* @provides phabricator-diff-changeset-list
* @requires javelin-install
* phuix-button-view
* @javelin
*/
JX.install('DiffChangesetList', {
construct: function() {
this._changesets = [];
var onload = JX.bind(this, this._ifawake, this._onload);
JX.Stratcom.listen('click', 'differential-load', onload);
var onmore = JX.bind(this, this._ifawake, this._onmore);
JX.Stratcom.listen('click', 'show-more', onmore);
var onmenu = JX.bind(this, this._ifawake, this._onmenu);
JX.Stratcom.listen('click', 'differential-view-options', onmenu);
var oncollapse = JX.bind(this, this._ifawake, this._oncollapse, true);
JX.Stratcom.listen('click', 'hide-inline', oncollapse);
var onexpand = JX.bind(this, this._ifawake, this._oncollapse, false);
JX.Stratcom.listen('click', 'reveal-inline', onexpand);
var onedit = JX.bind(this, this._ifawake, this._onaction, 'edit');
JX.Stratcom.listen(
'click',
['differential-inline-comment', 'differential-inline-edit'],
onedit);
var ondone = JX.bind(this, this._ifawake, this._onaction, 'done');
JX.Stratcom.listen(
'click',
['differential-inline-comment', 'differential-inline-done'],
ondone);
var ondelete = JX.bind(this, this._ifawake, this._onaction, 'delete');
JX.Stratcom.listen(
'click',
['differential-inline-comment', 'differential-inline-delete'],
ondelete);
var onreply = JX.bind(this, this._ifawake, this._onaction, 'reply');
JX.Stratcom.listen(
'click',
['differential-inline-comment', 'differential-inline-reply'],
onreply);
var onresize = JX.bind(this, this._ifawake, this._onresize);
JX.Stratcom.listen('resize', null, onresize);
var onscroll = JX.bind(this, this._ifawake, this._onscroll);
JX.Stratcom.listen('scroll', null, onscroll);
var onselect = JX.bind(this, this._ifawake, this._onselect);
JX.Stratcom.listen(
'mousedown',
['differential-inline-comment', 'differential-inline-header'],
onselect);
var onhover = JX.bind(this, this._ifawake, this._onhover);
JX.Stratcom.listen(
['mouseover', 'mouseout'],
'differential-inline-comment',
onhover);
var onrangedown = JX.bind(this, this._ifawake, this._onrangedown);
JX.Stratcom.listen(
'mousedown',
- ['differential-changeset', 'tag:th'],
+ ['differential-changeset', 'tag:td'],
onrangedown);
var onrangemove = JX.bind(this, this._ifawake, this._onrangemove);
JX.Stratcom.listen(
['mouseover', 'mouseout'],
- ['differential-changeset', 'tag:th'],
+ ['differential-changeset', 'tag:td'],
onrangemove);
var onrangeup = JX.bind(this, this._ifawake, this._onrangeup);
JX.Stratcom.listen(
'mouseup',
null,
onrangeup);
},
properties: {
translations: null,
inlineURI: null,
inlineListURI: null,
isStandalone: false
},
members: {
_initialized: false,
_asleep: true,
_changesets: null,
_cursorItem: null,
_focusNode: null,
_focusStart: null,
_focusEnd: null,
_hoverNode: null,
_hoverInline: null,
_hoverOrigin: null,
_hoverTarget: null,
_rangeActive: false,
_rangeOrigin: null,
_rangeTarget: null,
_bannerNode: null,
_unsavedButton: null,
_unsubmittedButton: null,
_doneButton: null,
_doneMode: null,
_dropdownMenu: null,
_menuButton: null,
_menuItems: null,
sleep: function() {
this._asleep = true;
this._redrawFocus();
this._redrawSelection();
this.resetHover();
this._bannerChangeset = null;
this._redrawBanner();
},
wake: function() {
this._asleep = false;
this._redrawFocus();
this._redrawSelection();
this._bannerChangeset = null;
this._redrawBanner();
if (this._initialized) {
return;
}
this._initialized = true;
var pht = this.getTranslations();
// We may be viewing the normal "/D123" view (with all the changesets)
// or the standalone view (with just one changeset). In the standalone
// view, some options (like jumping to next or previous file) do not
// make sense and do not function.
var standalone = this.getIsStandalone();
var label;
label = pht('Jump to next change.');
this._installJumpKey('j', label, 1);
label = pht('Jump to previous change.');
this._installJumpKey('k', label, -1);
if (!standalone) {
label = pht('Jump to next file.');
this._installJumpKey('J', label, 1, 'file');
label = pht('Jump to previous file.');
this._installJumpKey('K', label, -1, 'file');
}
label = pht('Jump to next inline comment.');
this._installJumpKey('n', label, 1, 'comment');
label = pht('Jump to previous inline comment.');
this._installJumpKey('p', label, -1, 'comment');
label = pht('Jump to next inline comment, including collapsed comments.');
this._installJumpKey('N', label, 1, 'comment', true);
label = pht(
'Jump to previous inline comment, including collapsed comments.');
this._installJumpKey('P', label, -1, 'comment', true);
if (!standalone) {
label = pht('Hide or show the current file.');
this._installKey('h', label, this._onkeytogglefile);
label = pht('Jump to the table of contents.');
this._installKey('t', label, this._ontoc);
}
label = pht('Reply to selected inline comment or change.');
this._installKey('r', label, JX.bind(this, this._onkeyreply, false));
label = pht('Reply and quote selected inline comment.');
this._installKey('R', label, JX.bind(this, this._onkeyreply, true));
label = pht('Edit selected inline comment.');
this._installKey('e', label, this._onkeyedit);
label = pht('Mark or unmark selected inline comment as done.');
this._installKey('w', label, this._onkeydone);
label = pht('Collapse or expand inline comment.');
this._installKey('q', label, this._onkeycollapse);
label = pht('Hide or show all inline comments.');
this._installKey('A', label, this._onkeyhideall);
},
isAsleep: function() {
return this._asleep;
},
newChangesetForNode: function(node) {
var changeset = JX.DiffChangeset.getForNode(node);
this._changesets.push(changeset);
changeset.setChangesetList(this);
return changeset;
},
getChangesetForNode: function(node) {
return JX.DiffChangeset.getForNode(node);
},
getInlineByID: function(id) {
var inline = null;
for (var ii = 0; ii < this._changesets.length; ii++) {
inline = this._changesets[ii].getInlineByID(id);
if (inline) {
break;
}
}
return inline;
},
_ifawake: function(f) {
// This function takes another function and only calls it if the
// changeset list is awake, so we basically just ignore events when we
// are asleep. This may move up the stack at some point as we do more
// with Quicksand/Sheets.
if (this.isAsleep()) {
return;
}
return f.apply(this, [].slice.call(arguments, 1));
},
_onload: function(e) {
var data = e.getNodeData('differential-load');
// NOTE: We can trigger a load from either an explicit "Load" link on
// the changeset, or by clicking a link in the table of contents. If
// the event was a table of contents link, we let the anchor behavior
// run normally.
if (data.kill) {
e.kill();
}
var node = JX.$(data.id);
var changeset = this.getChangesetForNode(node);
changeset.load();
// TODO: Move this into Changeset.
var routable = changeset.getRoutable();
if (routable) {
routable.setPriority(2000);
}
},
_installKey: function(key, label, handler) {
handler = JX.bind(this, this._ifawake, handler);
return new JX.KeyboardShortcut(key, label)
.setHandler(handler)
.register();
},
_installJumpKey: function(key, label, delta, filter, show_collapsed) {
filter = filter || null;
var options = {
filter: filter,
collapsed: show_collapsed
};
var handler = JX.bind(this, this._onjumpkey, delta, options);
return this._installKey(key, label, handler);
},
_ontoc: function(manager) {
var toc = JX.$('toc');
manager.scrollTo(toc);
},
getSelectedInline: function() {
var cursor = this._cursorItem;
if (cursor) {
if (cursor.type == 'comment') {
return cursor.target;
}
}
return null;
},
_onkeyreply: function(is_quote) {
var cursor = this._cursorItem;
if (cursor) {
if (cursor.type == 'comment') {
var inline = cursor.target;
if (inline.canReply()) {
this.setFocus(null);
var text;
if (is_quote) {
text = inline.getRawText();
text = '> ' + text.replace(/\n/g, '\n> ') + '\n\n';
} else {
text = '';
}
inline.reply(text);
return;
}
}
// If the keyboard cursor is selecting a range of lines, we may have
// a mixture of old and new changes on the selected rows. It is not
// entirely unambiguous what the user means when they say they want
// to reply to this, but we use this logic: reply on the new file if
// there are any new lines. Otherwise (if there are only removed
// lines) reply on the old file.
if (cursor.type == 'change') {
var origin = cursor.nodes.begin;
var target = cursor.nodes.end;
// The "origin" and "target" are entire rows, but we need to find
// a range of "<th />" nodes to actually create an inline, so go
// fishing.
var old_list = [];
var new_list = [];
var row = origin;
while (row) {
var header = row.firstChild;
while (header) {
- if (JX.DOM.isType(header, 'th')) {
+ if (this.getLineNumberFromHeader(header)) {
if (header.className.indexOf('old') !== -1) {
old_list.push(header);
} else if (header.className.indexOf('new') !== -1) {
new_list.push(header);
}
}
header = header.nextSibling;
}
if (row == target) {
break;
}
row = row.nextSibling;
}
var use_list;
if (new_list.length) {
use_list = new_list;
} else {
use_list = old_list;
}
var src = use_list[0];
var dst = use_list[use_list.length - 1];
cursor.changeset.newInlineForRange(src, dst);
this.setFocus(null);
return;
}
}
var pht = this.getTranslations();
this._warnUser(pht('You must select a comment or change to reply to.'));
},
_onkeyedit: function() {
var cursor = this._cursorItem;
if (cursor) {
if (cursor.type == 'comment') {
var inline = cursor.target;
if (inline.canEdit()) {
this.setFocus(null);
inline.edit();
return;
}
}
}
var pht = this.getTranslations();
this._warnUser(pht('You must select a comment to edit.'));
},
_onkeydone: function() {
var cursor = this._cursorItem;
if (cursor) {
if (cursor.type == 'comment') {
var inline = cursor.target;
if (inline.canDone()) {
this.setFocus(null);
inline.toggleDone();
return;
}
}
}
var pht = this.getTranslations();
this._warnUser(pht('You must select a comment to mark done.'));
},
_onkeytogglefile: function() {
var cursor = this._cursorItem;
if (cursor) {
if (cursor.type == 'file') {
cursor.changeset.toggleVisibility();
return;
}
}
var pht = this.getTranslations();
this._warnUser(pht('You must select a file to hide or show.'));
},
_onkeycollapse: function() {
var cursor = this._cursorItem;
if (cursor) {
if (cursor.type == 'comment') {
var inline = cursor.target;
if (inline.canCollapse()) {
this.setFocus(null);
inline.setCollapsed(!inline.isCollapsed());
return;
}
}
}
var pht = this.getTranslations();
this._warnUser(pht('You must select a comment to hide.'));
},
_onkeyhideall: function() {
var inlines = this._getInlinesByType();
if (inlines.visible.length) {
this._toggleInlines('all');
} else {
this._toggleInlines('show');
}
},
_warnUser: function(message) {
new JX.Notification()
.setContent(message)
.alterClassName('jx-notification-alert', true)
.setDuration(3000)
.show();
},
_onjumpkey: function(delta, options) {
var state = this._getSelectionState();
var filter = options.filter || null;
var collapsed = options.collapsed || false;
var wrap = options.wrap || false;
var attribute = options.attribute || null;
var show = options.show || false;
var cursor = state.cursor;
var items = state.items;
// If there's currently no selection and the user tries to go back,
// don't do anything.
if ((cursor === null) && (delta < 0)) {
return;
}
var did_wrap = false;
while (true) {
if (cursor === null) {
cursor = 0;
} else {
cursor = cursor + delta;
}
// If we've gone backward past the first change, bail out.
if (cursor < 0) {
return;
}
// If we've gone forward off the end of the list, figure out where we
// should end up.
if (cursor >= items.length) {
if (!wrap) {
// If we aren't wrapping around, we're done.
return;
}
if (did_wrap) {
// If we're already wrapped around, we're done.
return;
}
// Otherwise, wrap the cursor back to the top.
cursor = 0;
did_wrap = true;
}
// If we're selecting things of a particular type (like only files)
// and the next item isn't of that type, move past it.
if (filter !== null) {
if (items[cursor].type !== filter) {
continue;
}
}
// If the item is collapsed, don't select it when iterating with jump
// keys. It can still potentially be selected in other ways.
if (!collapsed) {
if (items[cursor].collapsed) {
continue;
}
}
// If the item has been deleted, don't select it when iterating. The
// cursor may remain on it until it is removed.
if (items[cursor].deleted) {
continue;
}
// If we're selecting things with a particular attribute, like
// "unsaved", skip items without the attribute.
if (attribute !== null) {
if (!(items[cursor].attributes || {})[attribute]) {
continue;
}
}
// If this item is a hidden inline but we're clicking a button which
// selects inlines of a particular type, make it visible again.
if (items[cursor].hidden) {
if (!show) {
continue;
}
items[cursor].target.setHidden(false);
}
// Otherwise, we've found a valid item to select.
break;
}
this._setSelectionState(items[cursor], true);
},
_getSelectionState: function() {
var items = this._getSelectableItems();
var cursor = null;
if (this._cursorItem !== null) {
for (var ii = 0; ii < items.length; ii++) {
var item = items[ii];
if (this._cursorItem.target === item.target) {
cursor = ii;
break;
}
}
}
return {
cursor: cursor,
items: items
};
},
_setSelectionState: function(item, scroll) {
this._cursorItem = item;
this._redrawSelection(scroll);
return this;
},
_redrawSelection: function(scroll) {
var cursor = this._cursorItem;
if (!cursor) {
this.setFocus(null);
return;
}
// If this item has been removed from the document (for example: create
// a new empty comment, then use the "Unsaved" button to select it, then
// cancel it), we can still keep the cursor here but do not want to show
// a selection reticle over an invisible node.
if (cursor.deleted) {
this.setFocus(null);
return;
}
this.setFocus(cursor.nodes.begin, cursor.nodes.end);
if (scroll) {
var pos = JX.$V(cursor.nodes.begin);
JX.DOM.scrollToPosition(0, pos.y - 60);
}
return this;
},
redrawCursor: function() {
// NOTE: This is setting the cursor to the current cursor. Usually, this
// would have no effect.
// However, if the old cursor pointed at an inline and the inline has
// been edited so the rows have changed, this updates the cursor to point
// at the new inline with the proper rows for the current state, and
// redraws the reticle correctly.
var state = this._getSelectionState();
if (state.cursor !== null) {
this._setSelectionState(state.items[state.cursor], false);
}
},
_getSelectableItems: function() {
var result = [];
for (var ii = 0; ii < this._changesets.length; ii++) {
var items = this._changesets[ii].getSelectableItems();
for (var jj = 0; jj < items.length; jj++) {
result.push(items[jj]);
}
}
return result;
},
_onhover: function(e) {
if (e.getIsTouchEvent()) {
return;
}
var inline;
if (e.getType() == 'mouseout') {
inline = null;
} else {
inline = this._getInlineForEvent(e);
}
this._setHoverInline(inline);
},
_onmore: function(e) {
e.kill();
var node = e.getNode('differential-changeset');
var changeset = this.getChangesetForNode(node);
var data = e.getNodeData('show-more');
var target = e.getNode('context-target');
changeset.loadContext(data.range, target);
},
_onmenu: function(e) {
var button = e.getNode('differential-view-options');
var data = JX.Stratcom.getData(button);
if (data.menu) {
// We've already built this menu, so we can let the menu itself handle
// the event.
return;
}
e.prevent();
var pht = this.getTranslations();
var node = JX.DOM.findAbove(
button,
'div',
'differential-changeset');
var changeset_list = this;
var changeset = this.getChangesetForNode(node);
var menu = new JX.PHUIXDropdownMenu(button);
var list = new JX.PHUIXActionListView();
var add_link = function(icon, name, href, local) {
if (!href) {
return;
}
var link = new JX.PHUIXActionView()
.setIcon(icon)
.setName(name)
.setHref(href)
.setHandler(function(e) {
if (local) {
window.location.assign(href);
} else {
window.open(href);
}
menu.close();
e.prevent();
});
list.addItem(link);
return link;
};
var reveal_item = new JX.PHUIXActionView()
.setIcon('fa-eye');
list.addItem(reveal_item);
var visible_item = new JX.PHUIXActionView()
.setHandler(function(e) {
e.prevent();
menu.close();
changeset.toggleVisibility();
});
list.addItem(visible_item);
add_link('fa-file-text', pht('Browse in Diffusion'), data.diffusionURI);
add_link('fa-file-o', pht('View Standalone'), data.standaloneURI);
var up_item = new JX.PHUIXActionView()
.setHandler(function(e) {
if (changeset.isLoaded()) {
// Don't let the user swap display modes if a comment is being
// edited, since they might lose their work. See PHI180.
var inlines = changeset.getInlines();
for (var ii = 0; ii < inlines.length; ii++) {
if (inlines[ii].isEditing()) {
changeset_list._warnUser(
pht(
'Finish editing inline comments before changing display ' +
'modes.'));
e.prevent();
menu.close();
return;
}
}
var renderer = changeset.getRenderer();
if (renderer == '1up') {
renderer = '2up';
} else {
renderer = '1up';
}
changeset.setRenderer(renderer);
}
changeset.reload();
e.prevent();
menu.close();
});
list.addItem(up_item);
var encoding_item = new JX.PHUIXActionView()
.setIcon('fa-font')
.setName(pht('Change Text Encoding...'))
.setHandler(function(e) {
var params = {
encoding: changeset.getEncoding()
};
new JX.Workflow('/services/encoding/', params)
.setHandler(function(r) {
changeset.setEncoding(r.encoding);
changeset.reload();
})
.start();
e.prevent();
menu.close();
});
list.addItem(encoding_item);
var highlight_item = new JX.PHUIXActionView()
.setIcon('fa-sun-o')
.setName(pht('Highlight As...'))
.setHandler(function(e) {
var params = {
highlight: changeset.getHighlight()
};
new JX.Workflow('/services/highlight/', params)
.setHandler(function(r) {
changeset.setHighlight(r.highlight);
changeset.reload();
})
.start();
e.prevent();
menu.close();
});
list.addItem(highlight_item);
add_link('fa-arrow-left', pht('Show Raw File (Left)'), data.leftURI);
add_link('fa-arrow-right', pht('Show Raw File (Right)'), data.rightURI);
add_link('fa-pencil', pht('Open in Editor'), data.editor, true);
add_link('fa-wrench', pht('Configure Editor'), data.editorConfigure);
menu.setContent(list.getNode());
menu.listen('open', function() {
// When the user opens the menu, check if there are any "Show More"
// links in the changeset body. If there aren't, disable the "Show
// Entire File" menu item since it won't change anything.
var nodes = JX.DOM.scry(JX.$(data.containerID), 'a', 'show-more');
if (nodes.length) {
reveal_item
.setDisabled(false)
.setName(pht('Show All Context'))
.setIcon('fa-file-o')
.setHandler(function(e) {
changeset.loadAllContext();
e.prevent();
menu.close();
});
} else {
reveal_item
.setDisabled(true)
.setIcon('fa-file')
.setName(pht('All Context Shown'))
.setHandler(function(e) { e.prevent(); });
}
encoding_item.setDisabled(!changeset.isLoaded());
highlight_item.setDisabled(!changeset.isLoaded());
if (changeset.isLoaded()) {
if (changeset.getRenderer() == '2up') {
up_item
.setIcon('fa-list-alt')
.setName(pht('View Unified'));
} else {
up_item
.setIcon('fa-files-o')
.setName(pht('View Side-by-Side'));
}
} else {
up_item
.setIcon('fa-refresh')
.setName(pht('Load Changes'));
}
visible_item
.setDisabled(true)
.setIcon('fa-expand')
.setName(pht('Can\'t Toggle Unloaded File'));
var diffs = JX.DOM.scry(
JX.$(data.containerID),
'table',
'differential-diff');
if (diffs.length > 1) {
JX.$E(
'More than one node with sigil "differential-diff" was found in "'+
data.containerID+'."');
} else if (diffs.length == 1) {
var diff = diffs[0];
visible_item.setDisabled(false);
if (!changeset.isVisible()) {
visible_item
.setName(pht('Expand File'))
.setIcon('fa-expand');
} else {
visible_item
.setName(pht('Collapse File'))
.setIcon('fa-compress');
}
} else {
// Do nothing when there is no diff shown in the table. For example,
// the file is binary.
}
});
data.menu = menu;
menu.open();
},
_oncollapse: function(is_collapse, e) {
e.kill();
var inline = this._getInlineForEvent(e);
inline.setCollapsed(is_collapse);
},
_onresize: function() {
this._redrawFocus();
this._redrawSelection();
this._redrawHover();
// Force a banner redraw after a resize event. Particularly, this makes
// sure the inline state updates immediately after an inline edit
// operation, even if the changeset itself has not changed.
this._bannerChangeset = null;
this._redrawBanner();
var changesets = this._changesets;
for (var ii = 0; ii < changesets.length; ii++) {
changesets[ii].redrawFileTree();
}
},
_onscroll: function() {
this._redrawBanner();
},
_onselect: function(e) {
// If the user clicked some element inside the header, like an action
// icon, ignore the event. They have to click the header element itself.
if (e.getTarget() !== e.getNode('differential-inline-header')) {
return;
}
var inline = this._getInlineForEvent(e);
if (!inline) {
return;
}
// The user definitely clicked an inline, so we're going to handle the
// event.
e.kill();
this.selectInline(inline);
},
selectInline: function(inline) {
var selection = this._getSelectionState();
var item;
// If the comment the user clicked is currently selected, deselect it.
// This makes it easy to undo things if you clicked by mistake.
if (selection.cursor !== null) {
item = selection.items[selection.cursor];
if (item.target === inline) {
this._setSelectionState(null, false);
return;
}
}
// Otherwise, select the item that the user clicked. This makes it
// easier to resume keyboard operations after using the mouse to do
// something else.
var items = selection.items;
for (var ii = 0; ii < items.length; ii++) {
item = items[ii];
if (item.target === inline) {
this._setSelectionState(item, false);
}
}
},
_onaction: function(action, e) {
e.kill();
var inline = this._getInlineForEvent(e);
var is_ref = false;
// If we don't have a natural inline object, the user may have clicked
// an action (like "Delete") inside a preview element at the bottom of
// the page.
// If they did, try to find an associated normal inline to act on, and
// pretend they clicked that instead. This makes the overall state of
// the page more consistent.
// However, there may be no normal inline (for example, because it is
// on a version of the diff which is not visible). In this case, we
// act by reference.
if (inline === null) {
var data = e.getNodeData('differential-inline-comment');
inline = this.getInlineByID(data.id);
if (inline) {
is_ref = true;
} else {
switch (action) {
case 'delete':
this._deleteInlineByID(data.id);
return;
}
}
}
// TODO: For normal operations, highlight the inline range here.
switch (action) {
case 'edit':
inline.edit();
break;
case 'done':
inline.toggleDone();
break;
case 'delete':
inline.delete(is_ref);
break;
case 'reply':
inline.reply();
break;
}
},
redrawPreview: function() {
// TODO: This isn't the cleanest way to find the preview form, but
// rendering no longer has direct access to it.
var forms = JX.DOM.scry(document.body, 'form', 'transaction-append');
if (forms.length) {
JX.DOM.invoke(forms[0], 'shouldRefresh');
}
// Clear the mouse hover reticle after a substantive edit: we don't get
// a "mouseout" event if the row vanished because of row being removed
// after an edit.
this.resetHover();
},
setFocus: function(node, extended_node) {
this._focusStart = node;
this._focusEnd = extended_node;
this._redrawFocus();
},
_redrawFocus: function() {
var node = this._focusStart;
var extended_node = this._focusEnd || node;
var reticle = this._getFocusNode();
if (!node || this.isAsleep()) {
JX.DOM.remove(reticle);
return;
}
// Outset the reticle some pixels away from the element, so there's some
// space between the focused element and the outline.
var p = JX.Vector.getPos(node);
var s = JX.Vector.getAggregateScrollForNode(node);
p.add(s).add(-4, -4).setPos(reticle);
// Compute the size we need to extend to the full extent of the focused
// nodes.
JX.Vector.getPos(extended_node)
.add(-p.x, -p.y)
.add(JX.Vector.getDim(extended_node))
.add(8, 8)
.setDim(reticle);
JX.DOM.getContentFrame().appendChild(reticle);
},
_getFocusNode: function() {
if (!this._focusNode) {
var node = JX.$N('div', {className : 'keyboard-focus-focus-reticle'});
this._focusNode = node;
}
return this._focusNode;
},
_setHoverInline: function(inline) {
this._hoverInline = inline;
if (inline) {
var changeset = inline.getChangeset();
var changeset_id;
var side = inline.getDisplaySide();
if (side == 'right') {
changeset_id = changeset.getRightChangesetID();
} else {
changeset_id = changeset.getLeftChangesetID();
}
var new_part;
if (inline.isNewFile()) {
new_part = 'N';
} else {
new_part = 'O';
}
var prefix = 'C' + changeset_id + new_part + 'L';
var number = inline.getLineNumber();
var length = inline.getLineLength();
try {
var origin = JX.$(prefix + number);
var target = JX.$(prefix + (number + length));
this._hoverOrigin = origin;
this._hoverTarget = target;
} catch (error) {
// There may not be any nodes present in the document. A case where
// this occurs is when you reply to a ghost inline which was made
// on lines near the bottom of "long.txt" in an earlier diff, and
// the file was later shortened so those lines no longer exist. For
// more details, see T11662.
this._hoverOrigin = null;
this._hoverTarget = null;
}
} else {
this._hoverOrigin = null;
this._hoverTarget = null;
}
this._redrawHover();
},
_setHoverRange: function(origin, target) {
this._hoverOrigin = origin;
this._hoverTarget = target;
this._redrawHover();
},
resetHover: function() {
this._setHoverInline(null);
this._hoverOrigin = null;
this._hoverTarget = null;
},
_redrawHover: function() {
var reticle = this._getHoverNode();
if (!this._hoverOrigin || this.isAsleep()) {
JX.DOM.remove(reticle);
return;
}
JX.DOM.getContentFrame().appendChild(reticle);
var top = this._hoverOrigin;
var bot = this._hoverTarget;
if (JX.$V(top).y > JX.$V(bot).y) {
var tmp = top;
top = bot;
bot = tmp;
}
// Find the leftmost cell that we're going to highlight: this is the next
// <td /> in the row. In 2up views, it should be directly adjacent. In
// 1up views, we may have to skip over the other line number column.
var l = top;
while (JX.DOM.isType(l, 'th')) {
l = l.nextSibling;
}
// Find the rightmost cell that we're going to highlight: this is the
// farthest consecutive, adjacent <td /> in the row. Sometimes the left
// and right nodes are the same (left side of 2up view); sometimes we're
// going to highlight several nodes (copy + code + coverage).
var r = l;
while (r.nextSibling && JX.DOM.isType(r.nextSibling, 'td')) {
r = r.nextSibling;
}
var pos = JX.$V(l)
.add(JX.Vector.getAggregateScrollForNode(l));
var dim = JX.$V(r)
.add(JX.Vector.getAggregateScrollForNode(r))
.add(-pos.x, -pos.y)
.add(JX.Vector.getDim(r));
var bpos = JX.$V(bot)
.add(JX.Vector.getAggregateScrollForNode(bot));
dim.y = (bpos.y - pos.y) + JX.Vector.getDim(bot).y;
pos.setPos(reticle);
dim.setDim(reticle);
JX.DOM.show(reticle);
},
_getHoverNode: function() {
if (!this._hoverNode) {
var attributes = {
className: 'differential-reticle'
};
this._hoverNode = JX.$N('div', attributes);
}
return this._hoverNode;
},
_deleteInlineByID: function(id) {
var uri = this.getInlineURI();
var data = {
op: 'refdelete',
id: id
};
var handler = JX.bind(this, this.redrawPreview);
new JX.Workflow(uri, data)
.setHandler(handler)
.start();
},
_getInlineForEvent: function(e) {
var node = e.getNode('differential-changeset');
if (!node) {
return null;
}
var changeset = this.getChangesetForNode(node);
var inline_row = e.getNode('inline-row');
return changeset.getInlineForRow(inline_row);
},
- getLineNumberFromHeader: function(th) {
+ getLineNumberFromHeader: function(node) {
+ var n = parseInt(node.getAttribute('data-n'));
+
+ if (!n) {
+ return null;
+ }
+
+ // If this is a line number that's part of a row showing more context,
+ // we don't want to let users leave inlines here.
+
try {
- return parseInt(th.id.match(/^C\d+[ON]L(\d+)$/)[1], 10);
- } catch (x) {
+ JX.DOM.findAbove(node, 'tr', 'context-target');
return null;
+ } catch (ex) {
+ // Ignore.
}
+
+ return n;
},
getDisplaySideFromHeader: function(th) {
return (th.parentNode.firstChild != th) ? 'right' : 'left';
},
_onrangedown: function(e) {
// NOTE: We're allowing "mousedown" from a touch event through so users
// can leave inlines on a single line.
// See PHI985. We want to exclude both right-mouse and middle-mouse
// clicks from continuing.
if (!e.isLeftButton()) {
return;
}
if (this._rangeActive) {
return;
}
var target = e.getTarget();
var number = this.getLineNumberFromHeader(target);
if (!number) {
return;
}
e.kill();
this._rangeActive = true;
this._rangeOrigin = target;
this._rangeTarget = target;
this._setHoverRange(this._rangeOrigin, this._rangeTarget);
},
_onrangemove: function(e) {
if (e.getIsTouchEvent()) {
return;
}
var is_out = (e.getType() == 'mouseout');
var target = e.getTarget();
this._updateRange(target, is_out);
},
_updateRange: function(target, is_out) {
- // Don't update the range if this "<th />" doesn't correspond to a line
+ // Don't update the range if this target doesn't correspond to a line
// number. For instance, this may be a dead line number, like the empty
// line numbers on the left hand side of a newly added file.
var number = this.getLineNumberFromHeader(target);
if (!number) {
return;
}
if (this._rangeActive) {
var origin = this._hoverOrigin;
// Don't update the reticle if we're selecting a line range and the
// "<th />" under the cursor is on the wrong side of the file. You can
// only leave inline comments on the left or right side of a file, not
// across lines on both sides.
var origin_side = this.getDisplaySideFromHeader(origin);
var target_side = this.getDisplaySideFromHeader(target);
if (origin_side != target_side) {
return;
}
// Don't update the reticle if we're selecting a line range and the
// "<th />" under the cursor corresponds to a different file. You can
// only leave inline comments on lines in a single file, not across
// multiple files.
var origin_table = JX.DOM.findAbove(origin, 'table');
var target_table = JX.DOM.findAbove(target, 'table');
if (origin_table != target_table) {
return;
}
}
if (is_out) {
if (this._rangeActive) {
// If we're dragging a range, just leave the state as it is. This
// allows you to drag over something invalid while selecting a
// range without the range flickering or getting lost.
} else {
// Otherwise, clear the current range.
this.resetHover();
}
return;
}
if (this._rangeActive) {
this._rangeTarget = target;
} else {
this._rangeOrigin = target;
this._rangeTarget = target;
}
this._setHoverRange(this._rangeOrigin, this._rangeTarget);
},
_onrangeup: function(e) {
if (!this._rangeActive) {
return;
}
e.kill();
var origin = this._rangeOrigin;
var target = this._rangeTarget;
// If the user dragged a range from the bottom to the top, swap the node
// order around.
if (JX.$V(origin).y > JX.$V(target).y) {
var tmp = target;
target = origin;
origin = tmp;
}
var node = JX.DOM.findAbove(origin, null, 'differential-changeset');
var changeset = this.getChangesetForNode(node);
changeset.newInlineForRange(origin, target);
this._rangeActive = false;
this._rangeOrigin = null;
this._rangeTarget = null;
this.resetHover();
},
_redrawBanner: function() {
// If the inline comment menu is open and we've done a redraw, close it.
// In particular, this makes it close when you scroll the document:
// otherwise, it stays open but the banner moves underneath it.
if (this._dropdownMenu) {
this._dropdownMenu.close();
}
var node = this._getBannerNode();
var changeset = this._getVisibleChangeset();
if (!changeset) {
this._bannerChangeset = null;
JX.DOM.remove(node);
return;
}
// Don't do anything if nothing has changed. This seems to avoid some
// flickering issues in Safari, at least.
if (this._bannerChangeset === changeset) {
return;
}
this._bannerChangeset = changeset;
var inlines = this._getInlinesByType();
var unsaved = inlines.unsaved;
var unsubmitted = inlines.unsubmitted;
var undone = inlines.undone;
var done = inlines.done;
var draft_done = inlines.draftDone;
JX.DOM.alterClass(
node,
'diff-banner-has-unsaved',
!!unsaved.length);
JX.DOM.alterClass(
node,
'diff-banner-has-unsubmitted',
!!unsubmitted.length);
JX.DOM.alterClass(
node,
'diff-banner-has-draft-done',
!!draft_done.length);
var pht = this.getTranslations();
var unsaved_button = this._getUnsavedButton();
var unsubmitted_button = this._getUnsubmittedButton();
var done_button = this._getDoneButton();
var menu_button = this._getMenuButton();
if (unsaved.length) {
unsaved_button.setText(unsaved.length + ' ' + pht('Unsaved'));
JX.DOM.show(unsaved_button.getNode());
} else {
JX.DOM.hide(unsaved_button.getNode());
}
if (unsubmitted.length || draft_done.length) {
var any_draft_count = unsubmitted.length + draft_done.length;
unsubmitted_button.setText(any_draft_count + ' ' + pht('Unsubmitted'));
JX.DOM.show(unsubmitted_button.getNode());
} else {
JX.DOM.hide(unsubmitted_button.getNode());
}
if (done.length || undone.length) {
// If you haven't marked any comments as "Done", we just show text
// like "3 Comments". If you've marked at least one done, we show
// "1 / 3 Comments".
var done_text;
if (done.length) {
done_text = [
done.length,
' / ',
(done.length + undone.length),
' ',
pht('Comments')
];
} else {
done_text = [
undone.length,
' ',
pht('Comments')
];
}
done_button.setText(done_text);
JX.DOM.show(done_button.getNode());
// If any comments are not marked "Done", this cycles through the
// missing comments. Otherwise, it cycles through all the saved
// comments.
if (undone.length) {
this._doneMode = 'undone';
} else {
this._doneMode = 'done';
}
} else {
JX.DOM.hide(done_button.getNode());
}
var path_view = [icon, ' ', changeset.getDisplayPath()];
var buttons_attrs = {
className: 'diff-banner-buttons'
};
var buttons_list = [
unsaved_button.getNode(),
unsubmitted_button.getNode(),
done_button.getNode(),
menu_button.getNode()
];
var buttons_view = JX.$N('div', buttons_attrs, buttons_list);
var icon = new JX.PHUIXIconView()
.setIcon(changeset.getIcon())
.getNode();
JX.DOM.setContent(node, [buttons_view, path_view]);
document.body.appendChild(node);
},
_getInlinesByType: function() {
var changesets = this._changesets;
var unsaved = [];
var unsubmitted = [];
var undone = [];
var done = [];
var draft_done = [];
var visible_done = [];
var visible_collapsed = [];
var visible_ghosts = [];
var visible = [];
var hidden = [];
for (var ii = 0; ii < changesets.length; ii++) {
var inlines = changesets[ii].getInlines();
var inline;
var jj;
for (jj = 0; jj < inlines.length; jj++) {
inline = inlines[jj];
if (inline.isDeleted()) {
continue;
}
if (inline.isSynthetic()) {
continue;
}
if (inline.isEditing()) {
unsaved.push(inline);
} else if (!inline.getID()) {
// These are new comments which have been cancelled, and do not
// count as anything.
continue;
} else if (inline.isDraft()) {
unsubmitted.push(inline);
} else {
// NOTE: Unlike other states, an inline may be marked with a
// draft checkmark and still be a "done" or "undone" comment.
if (inline.isDraftDone()) {
draft_done.push(inline);
}
if (!inline.isDone()) {
undone.push(inline);
} else {
done.push(inline);
}
}
}
for (jj = 0; jj < inlines.length; jj++) {
inline = inlines[jj];
if (inline.isDeleted()) {
continue;
}
if (inline.isEditing()) {
continue;
}
if (inline.isHidden()) {
hidden.push(inline);
continue;
}
visible.push(inline);
if (inline.isDone()) {
visible_done.push(inline);
}
if (inline.isCollapsed()) {
visible_collapsed.push(inline);
}
if (inline.isGhost()) {
visible_ghosts.push(inline);
}
}
}
return {
unsaved: unsaved,
unsubmitted: unsubmitted,
undone: undone,
done: done,
draftDone: draft_done,
visibleDone: visible_done,
visibleGhosts: visible_ghosts,
visibleCollapsed: visible_collapsed,
visible: visible,
hidden: hidden
};
},
_getUnsavedButton: function() {
if (!this._unsavedButton) {
var button = new JX.PHUIXButtonView()
.setIcon('fa-commenting-o')
.setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE);
var node = button.getNode();
var onunsaved = JX.bind(this, this._onunsavedclick);
JX.DOM.listen(node, 'click', null, onunsaved);
this._unsavedButton = button;
}
return this._unsavedButton;
},
_getUnsubmittedButton: function() {
if (!this._unsubmittedButton) {
var button = new JX.PHUIXButtonView()
.setIcon('fa-comment-o')
.setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE);
var node = button.getNode();
var onunsubmitted = JX.bind(this, this._onunsubmittedclick);
JX.DOM.listen(node, 'click', null, onunsubmitted);
this._unsubmittedButton = button;
}
return this._unsubmittedButton;
},
_getDoneButton: function() {
if (!this._doneButton) {
var button = new JX.PHUIXButtonView()
.setIcon('fa-comment')
.setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE);
var node = button.getNode();
var ondone = JX.bind(this, this._ondoneclick);
JX.DOM.listen(node, 'click', null, ondone);
this._doneButton = button;
}
return this._doneButton;
},
_getMenuButton: function() {
if (!this._menuButton) {
var pht = this.getTranslations();
var button = new JX.PHUIXButtonView()
.setIcon('fa-bars')
.setButtonType(JX.PHUIXButtonView.BUTTONTYPE_SIMPLE)
.setAuralLabel(pht('Display Options'));
var dropdown = new JX.PHUIXDropdownMenu(button.getNode());
this._menuItems = {};
var list = new JX.PHUIXActionListView();
dropdown.setContent(list.getNode());
var map = {
hideDone: {
type: 'done'
},
hideCollapsed: {
type: 'collapsed'
},
hideGhosts: {
type: 'ghosts'
},
hideAll: {
type: 'all'
},
showAll: {
type: 'show'
}
};
for (var k in map) {
var spec = map[k];
var handler = JX.bind(this, this._onhideinlines, spec.type);
var item = new JX.PHUIXActionView()
.setHandler(handler);
list.addItem(item);
this._menuItems[k] = item;
}
dropdown.listen('open', JX.bind(this, this._ondropdown));
if (this.getInlineListURI()) {
list.addItem(
new JX.PHUIXActionView()
.setDivider(true));
list.addItem(
new JX.PHUIXActionView()
.setIcon('fa-external-link')
.setName(pht('List Inline Comments'))
.setHref(this.getInlineListURI()));
}
this._menuButton = button;
this._dropdownMenu = dropdown;
}
return this._menuButton;
},
_ondropdown: function() {
var inlines = this._getInlinesByType();
var items = this._menuItems;
var pht = this.getTranslations();
items.hideDone
.setName(pht('Hide "Done" Inlines'))
.setDisabled(!inlines.visibleDone.length);
items.hideCollapsed
.setName(pht('Hide Collapsed Inlines'))
.setDisabled(!inlines.visibleCollapsed.length);
items.hideGhosts
.setName(pht('Hide Older Inlines'))
.setDisabled(!inlines.visibleGhosts.length);
items.hideAll
.setName(pht('Hide All Inlines'))
.setDisabled(!inlines.visible.length);
items.showAll
.setName(pht('Show All Inlines'))
.setDisabled(!inlines.hidden.length);
},
_onhideinlines: function(type, e) {
this._dropdownMenu.close();
e.prevent();
this._toggleInlines(type);
},
_toggleInlines: function(type) {
var inlines = this._getInlinesByType();
// Clear the selection state since we end up in a weird place if the
// user hides the selected inline.
this._setSelectionState(null);
var targets;
var mode = true;
switch (type) {
case 'done':
targets = inlines.visibleDone;
break;
case 'collapsed':
targets = inlines.visibleCollapsed;
break;
case 'ghosts':
targets = inlines.visibleGhosts;
break;
case 'all':
targets = inlines.visible;
break;
case 'show':
targets = inlines.hidden;
mode = false;
break;
}
for (var ii = 0; ii < targets.length; ii++) {
targets[ii].setHidden(mode);
}
},
_onunsavedclick: function(e) {
e.kill();
var options = {
filter: 'comment',
wrap: true,
show: true,
attribute: 'unsaved'
};
this._onjumpkey(1, options);
},
_onunsubmittedclick: function(e) {
e.kill();
var options = {
filter: 'comment',
wrap: true,
show: true,
attribute: 'anyDraft'
};
this._onjumpkey(1, options);
},
_ondoneclick: function(e) {
e.kill();
var options = {
filter: 'comment',
wrap: true,
show: true,
attribute: this._doneMode
};
this._onjumpkey(1, options);
},
_getBannerNode: function() {
if (!this._bannerNode) {
var attributes = {
className: 'diff-banner',
id: 'diff-banner'
};
this._bannerNode = JX.$N('div', attributes);
}
return this._bannerNode;
},
_getVisibleChangeset: function() {
if (this.isAsleep()) {
return null;
}
if (JX.Device.getDevice() != 'desktop') {
return null;
}
// Never show the banner if we're very near the top of the page.
var margin = 480;
var s = JX.Vector.getScroll();
if (s.y < margin) {
return null;
}
// We're going to find the changeset which spans an invisible line a
// little underneath the bottom of the banner. This makes the header
// tick over from "A.txt" to "B.txt" just as "A.txt" scrolls completely
// offscreen.
var detect_height = 64;
for (var ii = 0; ii < this._changesets.length; ii++) {
var changeset = this._changesets[ii];
var c = changeset.getVectors();
// If the changeset starts above the line...
if (c.pos.y <= (s.y + detect_height)) {
// ...and ends below the line, this is the current visible changeset.
if ((c.pos.y + c.dim.y) >= (s.y + detect_height)) {
return changeset;
}
}
}
return null;
}
}
});
diff --git a/webroot/rsrc/js/application/differential/behavior-user-select.js b/webroot/rsrc/js/application/differential/behavior-user-select.js
deleted file mode 100644
index 8db48b704..000000000
--- a/webroot/rsrc/js/application/differential/behavior-user-select.js
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * @provides javelin-behavior-differential-user-select
- * @requires javelin-behavior
- * javelin-dom
- * javelin-stratcom
- */
-
-JX.behavior('differential-user-select', function() {
-
- var unselectable;
-
- function isOnRight(node) {
- return node.previousSibling &&
- node.parentNode.firstChild != node.previousSibling;
- }
-
- JX.Stratcom.listen(
- 'mousedown',
- null,
- function(e) {
- var key = 'differential-unselectable';
- if (unselectable) {
- JX.DOM.alterClass(unselectable, key, false);
- }
- var diff = e.getNode('differential-diff');
- var td = e.getNode('tag:td');
- if (diff && td && isOnRight(td)) {
- unselectable = diff;
- JX.DOM.alterClass(diff, key, true);
- }
- });
-
-});
diff --git a/webroot/rsrc/js/application/diffusion/behavior-commit-graph.js b/webroot/rsrc/js/application/diffusion/behavior-commit-graph.js
index 309f97232..5c4591b54 100644
--- a/webroot/rsrc/js/application/diffusion/behavior-commit-graph.js
+++ b/webroot/rsrc/js/application/diffusion/behavior-commit-graph.js
@@ -1,146 +1,154 @@
/**
* @provides javelin-behavior-diffusion-commit-graph
* @requires javelin-behavior
* javelin-dom
* javelin-stratcom
*/
JX.behavior('diffusion-commit-graph', function(config) {
var nodes = JX.DOM.scry(document.body, 'div', 'commit-graph');
var cxt;
// Pick the color for column 'c'.
function color(c) {
var colors = [
'#cc0000',
'#cc0099',
'#6600cc',
'#0033cc',
'#00cccc',
'#00cc33',
'#66cc00',
'#cc9900'
];
return colors[c % colors.length];
}
// Stroke a line (for lines between commits).
function lstroke(c) {
cxt.lineWidth = 3;
cxt.strokeStyle = '#ffffff';
cxt.stroke();
cxt.lineWidth = 1;
cxt.strokeStyle = color(c);
cxt.stroke();
}
// Stroke with fill (for commit circles).
function fstroke(c) {
cxt.lineWidth = 1;
cxt.fillStyle = color(c);
cxt.strokeStyle = '#ffffff';
cxt.fill();
cxt.stroke();
}
+ // If the graph is going to be wide, squish it a bit so it doesn't take up
+ // quite as much space.
+ var default_width;
+ if (config.count >= 8) {
+ default_width = 6;
+ } else {
+ default_width = 12;
+ }
for (var ii = 0; ii < nodes.length; ii++) {
var data = JX.Stratcom.getData(nodes[ii]);
- var cell = 12; // Width of each thread.
+ var cell = default_width;
var xpos = function(col) {
return (col * cell) + (cell / 2);
};
var h = 34;
var w = cell * config.count;
var canvas = JX.$N('canvas', {width: w, height: h});
cxt = canvas.getContext('2d');
cxt.lineWidth = 3;
// This gives us sharper lines, since lines drawn on an integer (like 5)
// are drawn from 4.5 to 5.5.
cxt.translate(0.5, 0.5);
cxt.strokeStyle = '#ffffff';
cxt.fillStyle = '#ffffff';
// First, figure out which column this commit appears in. It is marked by
// "o" (if it has a commit after it) or "^" (if no other commit has it as
// a parent). We use this to figure out where to draw the join/split lines.
var origin = null;
var jj;
var x;
var c;
for (jj = 0; jj < data.line.length; jj++) {
c = data.line.charAt(jj);
switch (c) {
case 'o':
case 'x':
case '^':
origin = xpos(jj);
break;
}
}
// Draw all the join lines. These start at some column at the top of the
// canvas and join the commit's column. They indicate branching.
for (jj = 0; jj < data.join.length; jj++) {
var join = data.join[jj];
x = xpos(join);
cxt.beginPath();
cxt.moveTo(x, 0);
cxt.bezierCurveTo(x, h/4, origin, h/4, origin, h/2);
lstroke(join);
}
// Draw all the split lines. These start at the commit and end at some
// column on the bottom of the canvas. They indicate merging.
for (jj = 0; jj < data.split.length; jj++) {
var split = data.split[jj];
x = xpos(split);
cxt.beginPath();
cxt.moveTo(origin, h/2);
cxt.bezierCurveTo(origin, 3*h/4, x, 3*h/4, x, h);
lstroke(split);
}
// Draw the vertical lines (a branch with no activity at this commit) and
// the commit circles.
for (jj = 0; jj < data.line.length; jj++) {
c = data.line.charAt(jj);
switch (c) {
case 'o':
case '^':
case '|':
case 'x':
case 'X':
if (c !== 'X') {
cxt.beginPath();
cxt.moveTo(xpos(jj), (c == '^' ? h/2 : 0));
cxt.lineTo(xpos(jj), (c == 'x' ? h/2 : h));
lstroke(jj);
}
if (c == 'o' || c == '^' || c == 'x' || c == 'X') {
cxt.beginPath();
cxt.arc(xpos(jj), h/2, 3, 0, 2 * Math.PI, true);
fstroke(jj);
}
break;
}
}
JX.DOM.setContent(nodes[ii], canvas);
}
});
diff --git a/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js b/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js
index b62f40a58..b64abc350 100644
--- a/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js
+++ b/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js
@@ -1,173 +1,160 @@
/**
* @provides javelin-behavior-maniphest-batch-selector
* @requires javelin-behavior
* javelin-dom
* javelin-stratcom
* javelin-util
*/
JX.behavior('maniphest-batch-selector', function(config) {
var selected = {};
// Test if a task node is selected.
var get_id = function(task) {
return JX.Stratcom.getData(task).taskID;
};
var is_selected = function(task) {
return (get_id(task) in selected);
};
// Change the selected state of a task.
var change = function(task, to) {
if (to === undefined) {
to = !is_selected(task);
}
if (to) {
selected[get_id(task)] = true;
} else {
delete selected[get_id(task)];
}
JX.DOM.alterClass(
task,
'phui-oi-selected',
is_selected(task));
update();
};
- var redraw = function (task) {
- var selected = is_selected(task);
- change(task, selected);
- };
- JX.Stratcom.listen(
- 'subpriority-changed',
- null,
- function (e) {
- e.kill();
- var data = e.getData();
- redraw(data.task);
- });
-
// Change all tasks to some state (used by "select all" / "clear selection"
// buttons).
var changeall = function(to) {
var inputs = JX.DOM.scry(document.body, 'li', 'maniphest-task');
for (var ii = 0; ii < inputs.length; ii++) {
change(inputs[ii], to);
}
};
// Clear any document text selection after toggling a task via shift click,
// since errant clicks tend to start selecting various ranges otherwise.
var clear_selection = function() {
if (window.getSelection) {
if (window.getSelection().empty) {
window.getSelection().empty();
} else if (window.getSelection().removeAllRanges) {
window.getSelection().removeAllRanges();
}
} else if (document.selection) {
document.selection.empty();
}
};
// Update the status text showing how many tasks are selected, and the button
// state.
var update = function() {
var count = JX.keys(selected).length;
var status;
if (count === 0) {
status = 'Shift-Click to Select Tasks';
} else if (status == 1) {
status = '1 Selected Task';
} else {
status = count + ' Selected Tasks';
}
JX.DOM.setContent(JX.$(config.status), status);
var submit = JX.$(config.submit);
var disable = (count === 0);
submit.disabled = disable;
JX.DOM.alterClass(submit, 'disabled', disable);
};
// When he user shift-clicks the task, update the rest of the application
// state.
JX.Stratcom.listen(
'click',
'maniphest-task',
function(e) {
var raw = e.getRawEvent();
if (!raw.shiftKey) {
return;
}
if (raw.ctrlKey || raw.altKey || raw.metaKey || e.isRightButton()) {
return;
}
if (JX.Stratcom.pass(e)) {
return;
}
e.kill();
change(e.getNode('maniphest-task'));
clear_selection();
});
// When the user clicks "Select All", select all tasks.
JX.DOM.listen(
JX.$(config.selectNone),
'click',
null,
function(e) {
changeall(false);
e.kill();
});
// When the user clicks "Clear Selection", clear the selection.
JX.DOM.listen(
JX.$(config.selectAll),
'click',
null,
function(e) {
changeall(true);
e.kill();
});
// When the user submits the form, dump selected state into it.
JX.DOM.listen(
JX.$(config.formID),
'submit',
null,
function() {
var ids = [];
for (var k in selected) {
ids.push(k);
}
ids = ids.join(',');
var input = JX.$N('input', {type: 'hidden', name: 'ids', value: ids});
JX.DOM.setContent(JX.$(config.idContainer), input);
});
update();
});
diff --git a/webroot/rsrc/js/application/maniphest/behavior-subpriorityeditor.js b/webroot/rsrc/js/application/maniphest/behavior-subpriorityeditor.js
deleted file mode 100644
index 82f16854f..000000000
--- a/webroot/rsrc/js/application/maniphest/behavior-subpriorityeditor.js
+++ /dev/null
@@ -1,72 +0,0 @@
-/**
- * @provides javelin-behavior-maniphest-subpriority-editor
- * @requires javelin-behavior
- * javelin-dom
- * javelin-stratcom
- * javelin-workflow
- * phabricator-draggable-list
- */
-
-JX.behavior('maniphest-subpriority-editor', function(config) {
-
- var draggable = new JX.DraggableList('maniphest-task')
- .setFindItemsHandler(function() {
- var tasks = JX.DOM.scry(document.body, 'li', 'maniphest-task');
- var heads = JX.DOM.scry(document.body, 'div', 'task-group');
- return tasks.concat(heads);
- })
- .setGhostHandler(function(ghost, target) {
- if (!target) {
- // The user is trying to drag a task above the first group header;
- // don't permit that since it doesn't make sense.
- return false;
- }
-
- if (target.nextSibling) {
- if (JX.DOM.isType(target, 'div')) {
- target.nextSibling.insertBefore(ghost, target.nextSibling.firstChild);
- } else {
- target.parentNode.insertBefore(ghost, target.nextSibling);
- }
- } else {
- target.parentNode.appendChild(ghost);
- }
- });
-
- draggable.listen('shouldBeginDrag', function(e) {
- if (e.getNode('slippery') || e.getNode('maniphest-edit-task')) {
- JX.Stratcom.context().kill();
- }
- });
-
- draggable.listen('didDrop', function(node, after) {
- var data = {
- task: JX.Stratcom.getData(node).taskID
- };
-
- if (JX.DOM.isType(after, 'div')) {
- data.priority = JX.Stratcom.getData(after).priority;
- } else {
- data.after = JX.Stratcom.getData(after).taskID;
- }
-
- draggable.lock();
- JX.DOM.alterClass(node, 'drag-sending', true);
-
- var onresponse = function(r) {
- var nodes = JX.$H(r.tasks).getFragment().firstChild;
- var task = JX.DOM.find(nodes, 'li', 'maniphest-task');
- JX.DOM.replace(node, task);
- draggable.unlock();
- JX.Stratcom.invoke(
- 'subpriority-changed',
- null,
- { 'task' : task });
- };
-
- new JX.Workflow(config.uri, data)
- .setHandler(onresponse)
- .start();
- });
-
-});
diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js
index cac35c2d9..74c0bdf23 100644
--- a/webroot/rsrc/js/application/projects/WorkboardBoard.js
+++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js
@@ -1,275 +1,670 @@
/**
* @provides javelin-workboard-board
* @requires javelin-install
* javelin-dom
* javelin-util
* javelin-stratcom
* javelin-workflow
* phabricator-draggable-list
* javelin-workboard-column
+ * javelin-workboard-header-template
+ * javelin-workboard-card-template
+ * javelin-workboard-order-template
* @javelin
*/
JX.install('WorkboardBoard', {
construct: function(controller, phid, root) {
this._controller = controller;
this._phid = phid;
this._root = root;
- this._templates = {};
- this._orderMaps = {};
- this._propertiesMap = {};
+ this._headers = {};
+ this._cards = {};
+ this._orders = {};
+
this._buildColumns();
},
properties: {
order: null,
pointsEnabled: false
},
members: {
_controller: null,
_phid: null,
_root: null,
_columns: null,
- _templates: null,
- _orderMaps: null,
- _propertiesMap: null,
+ _headers: null,
+ _cards: null,
+ _dropPreviewNode: null,
+ _dropPreviewListNode: null,
+ _previewPHID: null,
+ _hidePreivew: false,
+ _previewPositionVector: null,
+ _previewDimState: false,
getRoot: function() {
return this._root;
},
getColumns: function() {
return this._columns;
},
getColumn: function(k) {
return this._columns[k];
},
getPHID: function() {
return this._phid;
},
- setCardTemplate: function(phid, template) {
- this._templates[phid] = template;
- return this;
+ getCardTemplate: function(phid) {
+ if (!this._cards[phid]) {
+ this._cards[phid] = new JX.WorkboardCardTemplate(phid);
+ }
+
+ return this._cards[phid];
},
- setObjectProperties: function(phid, properties) {
- this._propertiesMap[phid] = properties;
- return this;
+ getHeaderTemplate: function(header_key) {
+ if (!this._headers[header_key]) {
+ this._headers[header_key] = new JX.WorkboardHeaderTemplate(header_key);
+ }
+
+ return this._headers[header_key];
},
- getObjectProperties: function(phid) {
- return this._propertiesMap[phid];
+ getOrderTemplate: function(order_key) {
+ if (!this._orders[order_key]) {
+ this._orders[order_key] = new JX.WorkboardOrderTemplate(order_key);
+ }
+
+ return this._orders[order_key];
},
- getCardTemplate: function(phid) {
- return this._templates[phid];
+ getHeaderTemplatesForOrder: function(order) {
+ var templates = [];
+
+ for (var k in this._headers) {
+ var header = this._headers[k];
+
+ if (header.getOrder() !== order) {
+ continue;
+ }
+
+ templates.push(header);
+ }
+
+ templates.sort(JX.bind(this, this._sortHeaderTemplates));
+
+ return templates;
+ },
+
+ _sortHeaderTemplates: function(u, v) {
+ return this.compareVectors(u.getVector(), v.getVector());
},
getController: function() {
return this._controller;
},
- setOrderMap: function(phid, map) {
- this._orderMaps[phid] = map;
- return this;
- },
+ compareVectors: function(u_vec, v_vec) {
+ for (var ii = 0; ii < u_vec.length; ii++) {
+ if (u_vec[ii] > v_vec[ii]) {
+ return 1;
+ }
- getOrderVector: function(phid, key) {
- return this._orderMaps[phid][key];
+ if (u_vec[ii] < v_vec[ii]) {
+ return -1;
+ }
+ }
+
+ return 0;
},
start: function() {
this._setupDragHandlers();
for (var k in this._columns) {
this._columns[k].redraw();
}
},
_buildColumns: function() {
var nodes = JX.DOM.scry(this.getRoot(), 'ul', 'project-column');
this._columns = {};
for (var ii = 0; ii < nodes.length; ii++) {
var node = nodes[ii];
var data = JX.Stratcom.getData(node);
var phid = data.columnPHID;
this._columns[phid] = new JX.WorkboardColumn(this, phid, node);
}
+
+ var on_over = JX.bind(this, this._showTriggerPreview);
+ var on_out = JX.bind(this, this._hideTriggerPreview);
+ JX.Stratcom.listen('mouseover', 'trigger-preview', on_over);
+ JX.Stratcom.listen('mouseout', 'trigger-preview', on_out);
+
+ var on_move = JX.bind(this, this._dimPreview);
+ JX.Stratcom.listen('mousemove', null, on_move);
+ },
+
+ _dimPreview: function(e) {
+ var p = this._previewPositionVector;
+ if (!p) {
+ return;
+ }
+
+ // When the mouse cursor gets near the drop preview element, fade it
+ // out so you can see through it. We can't do this with ":hover" because
+ // we disable cursor events.
+
+ var cursor = JX.$V(e);
+ var margin = 64;
+
+ var near_x = (cursor.x > (p.x - margin));
+ var near_y = (cursor.y > (p.y - margin));
+ var should_dim = (near_x && near_y);
+
+ this._setPreviewDimState(should_dim);
+ },
+
+ _setPreviewDimState: function(is_dim) {
+ if (is_dim === this._previewDimState) {
+ return;
+ }
+
+ this._previewDimState = is_dim;
+ var node = this._getDropPreviewNode();
+ JX.DOM.alterClass(node, 'workboard-drop-preview-fade', is_dim);
+ },
+
+ _showTriggerPreview: function(e) {
+ if (this._disablePreview) {
+ return;
+ }
+
+ var target = e.getTarget();
+ var node = e.getNode('trigger-preview');
+
+ if (target !== node) {
+ return;
+ }
+
+ var phid = JX.Stratcom.getData(node).columnPHID;
+ var column = this._columns[phid];
+
+ // Bail out if we don't know anything about this column.
+ if (!column) {
+ return;
+ }
+
+ if (phid === this._previewPHID) {
+ return;
+ }
+
+ this._previewPHID = phid;
+
+ var effects = column.getDropEffects();
+
+ var triggers = [];
+ for (var ii = 0; ii < effects.length; ii++) {
+ if (effects[ii].getIsTriggerEffect()) {
+ triggers.push(effects[ii]);
+ }
+ }
+
+ if (triggers.length) {
+ var header = column.getTriggerPreviewEffect();
+ triggers = [header].concat(triggers);
+ }
+
+ this._showEffects(triggers);
+ },
+
+ _hideTriggerPreview: function(e) {
+ if (this._disablePreview) {
+ return;
+ }
+
+ var target = e.getTarget();
+
+ if (target !== e.getNode('trigger-preview')) {
+ return;
+ }
+
+ this._removeTriggerPreview();
+ },
+
+ _removeTriggerPreview: function() {
+ this._showEffects([]);
+ this._previewPHID = null;
+ },
+
+ _beginDrag: function() {
+ this._disablePreview = true;
+ this._showEffects([]);
+ },
+
+ _endDrag: function() {
+ this._disablePreview = false;
},
_setupDragHandlers: function() {
var columns = this.getColumns();
+ var order_template = this.getOrderTemplate(this.getOrder());
+ var has_headers = order_template.getHasHeaders();
+ var can_reorder = order_template.getCanReorder();
+
var lists = [];
for (var k in columns) {
var column = columns[k];
- var list = new JX.DraggableList('project-card', column.getRoot())
+ var list = new JX.DraggableList('draggable-card', column.getRoot())
.setOuterContainer(this.getRoot())
- .setFindItemsHandler(JX.bind(column, column.getCardNodes))
+ .setFindItemsHandler(JX.bind(column, column.getDropTargetNodes))
.setCanDragX(true)
- .setHasInfiniteHeight(true);
+ .setHasInfiniteHeight(true)
+ .setIsDropTargetHandler(JX.bind(column, column.setIsDropTarget));
+
+ var default_handler = list.getGhostHandler();
+ list.setGhostHandler(
+ JX.bind(column, column.handleDragGhost, default_handler));
+
+ // The "compare handler" locks cards into a specific position in the
+ // column.
+ list.setCompareHandler(JX.bind(column, column.compareHandler));
+
+ // If the view has group headers, we lock cards into the right position
+ // when moving them between columns, but not within a column.
+ if (has_headers) {
+ list.setCompareOnMove(true);
+ }
+
+ // If we can't reorder cards, we always lock them into their current
+ // position.
+ if (!can_reorder) {
+ list.setCompareOnMove(true);
+ list.setCompareOnReorder(true);
+ }
+
+ list.setTargetChangeHandler(JX.bind(this, this._didChangeDropTarget));
list.listen('didDrop', JX.bind(this, this._onmovecard, list));
+ list.listen('didBeginDrag', JX.bind(this, this._beginDrag));
+ list.listen('didEndDrag', JX.bind(this, this._endDrag));
+
lists.push(list);
}
for (var ii = 0; ii < lists.length; ii++) {
lists[ii].setGroup(lists);
}
},
+ _didChangeDropTarget: function(src_list, src_node, dst_list, dst_node) {
+ if (!dst_list) {
+ // The card is being dragged into a dead area, like the left menu.
+ this._showEffects([]);
+ return;
+ }
+
+ if (dst_node === false) {
+ // The card is being dragged over itself, so dropping it won't
+ // affect anything.
+ this._showEffects([]);
+ return;
+ }
+
+ var src_phid = JX.Stratcom.getData(src_list.getRootNode()).columnPHID;
+ var dst_phid = JX.Stratcom.getData(dst_list.getRootNode()).columnPHID;
+
+ var src_column = this.getColumn(src_phid);
+ var dst_column = this.getColumn(dst_phid);
+
+ var effects = [];
+ if (src_column !== dst_column) {
+ effects = effects.concat(dst_column.getDropEffects());
+ }
+
+ var context = this._getDropContext(dst_node);
+ if (context.headerKey) {
+ var header = this.getHeaderTemplate(context.headerKey);
+ effects = effects.concat(header.getDropEffects());
+ }
+
+ var card_phid = JX.Stratcom.getData(src_node).objectPHID;
+ var card = src_column.getCard(card_phid);
+
+ var visible = [];
+ for (var ii = 0; ii < effects.length; ii++) {
+ if (effects[ii].isEffectVisibleForCard(card)) {
+ visible.push(effects[ii]);
+ }
+ }
+ effects = visible;
+
+ this._showEffects(effects);
+ },
+
+ _showEffects: function(effects) {
+ var node = this._getDropPreviewNode();
+
+ if (!effects.length) {
+ JX.DOM.remove(node);
+ this._previewPositionVector = null;
+ return;
+ }
+
+ var items = [];
+ for (var ii = 0; ii < effects.length; ii++) {
+ var effect = effects[ii];
+ items.push(effect.newNode());
+ }
+
+ JX.DOM.setContent(this._getDropPreviewListNode(), items);
+ document.body.appendChild(node);
+
+ // Undim the drop preview element if it was previously dimmed.
+ this._setPreviewDimState(false);
+ this._previewPositionVector = JX.$V(node);
+ },
+
+ _getDropPreviewNode: function() {
+ if (!this._dropPreviewNode) {
+ var attributes = {
+ className: 'workboard-drop-preview'
+ };
+
+ var content = [
+ this._getDropPreviewListNode()
+ ];
+
+ this._dropPreviewNode = JX.$N('div', attributes, content);
+ }
+
+ return this._dropPreviewNode;
+ },
+
+ _getDropPreviewListNode: function() {
+ if (!this._dropPreviewListNode) {
+ var attributes = {};
+ this._dropPreviewListNode = JX.$N('ul', attributes);
+ }
+
+ return this._dropPreviewListNode;
+ },
+
_findCardsInColumn: function(column_node) {
return JX.DOM.scry(column_node, 'li', 'project-card');
},
+ _getDropContext: function(after_node, item) {
+ var header_key;
+ var after_phids = [];
+ var before_phids = [];
+
+ // We're going to send an "afterPHID" and a "beforePHID" if the card
+ // was dropped immediately adjacent to another card. If a card was
+ // dropped before or after a header, we don't send a PHID for the card
+ // on the other side of the header.
+
+ // If the view has headers, we always send the header the card was
+ // dropped under.
+
+ var after_data;
+ var after_card = after_node;
+ while (after_card) {
+ after_data = JX.Stratcom.getData(after_card);
+
+ if (after_data.headerKey) {
+ break;
+ }
+
+ if (after_data.objectPHID) {
+ after_phids.push(after_data.objectPHID);
+ }
+
+ after_card = after_card.previousSibling;
+ }
+
+ if (item) {
+ var before_data;
+ var before_card = item.nextSibling;
+ while (before_card) {
+ before_data = JX.Stratcom.getData(before_card);
+
+ if (before_data.headerKey) {
+ break;
+ }
+
+ if (before_data.objectPHID) {
+ before_phids.push(before_data.objectPHID);
+ }
+
+ before_card = before_card.nextSibling;
+ }
+ }
+
+ var header_data;
+ var header_node = after_node;
+ while (header_node) {
+ header_data = JX.Stratcom.getData(header_node);
+ if (header_data.headerKey) {
+ break;
+ }
+ header_node = header_node.previousSibling;
+ }
+
+ if (header_data) {
+ header_key = header_data.headerKey;
+ }
+
+ return {
+ headerKey: header_key,
+ afterPHIDs: after_phids,
+ beforePHIDs: before_phids
+ };
+ },
+
_onmovecard: function(list, item, after_node, src_list) {
list.lock();
JX.DOM.alterClass(item, 'drag-sending', true);
var src_phid = JX.Stratcom.getData(src_list.getRootNode()).columnPHID;
var dst_phid = JX.Stratcom.getData(list.getRootNode()).columnPHID;
var item_phid = JX.Stratcom.getData(item).objectPHID;
var data = {
objectPHID: item_phid,
columnPHID: dst_phid,
order: this.getOrder()
};
- if (after_node) {
- data.afterPHID = JX.Stratcom.getData(after_node).objectPHID;
- }
+ var context = this._getDropContext(after_node, item);
+ data.afterPHIDs = context.afterPHIDs.join(',');
+ data.beforePHIDs = context.beforePHIDs.join(',');
- var before_node = item.nextSibling;
- if (before_node) {
- var before_phid = JX.Stratcom.getData(before_node).objectPHID;
- if (before_phid) {
- data.beforePHID = before_phid;
- }
+ if (context.headerKey) {
+ var properties = this.getHeaderTemplate(context.headerKey)
+ .getEditProperties();
+ data.header = JX.JSON.stringify(properties);
}
var visible_phids = [];
var column = this.getColumn(dst_phid);
for (var object_phid in column.getCards()) {
visible_phids.push(object_phid);
}
data.visiblePHIDs = visible_phids.join(',');
+ // If the user cancels the workflow (for example, by hitting an MFA
+ // prompt that they click "Cancel" on), put the card back where it was
+ // and reset the UI state.
+ var on_revert = JX.bind(
+ this,
+ this._revertCard,
+ list,
+ item,
+ src_phid,
+ dst_phid);
+
+ var after_phid = null;
+ if (data.afterPHIDs.length) {
+ after_phid = data.afterPHIDs[0];
+ }
+
var onupdate = JX.bind(
this,
this._oncardupdate,
list,
src_phid,
dst_phid,
- data.afterPHID);
+ after_phid);
new JX.Workflow(this.getController().getMoveURI(), data)
.setHandler(onupdate)
+ .setCloseHandler(on_revert)
.start();
},
+ _revertCard: function(list, item, src_phid, dst_phid) {
+ JX.DOM.alterClass(item, 'drag-sending', false);
+
+ var src_column = this.getColumn(src_phid);
+ var dst_column = this.getColumn(dst_phid);
+
+ src_column.markForRedraw();
+ dst_column.markForRedraw();
+ this._redrawColumns();
+
+ list.unlock();
+ },
+
_oncardupdate: function(list, src_phid, dst_phid, after_phid, response) {
var src_column = this.getColumn(src_phid);
var dst_column = this.getColumn(dst_phid);
var card = src_column.removeCard(response.objectPHID);
dst_column.addCard(card, after_phid);
src_column.markForRedraw();
dst_column.markForRedraw();
this.updateCard(response);
+ var sounds = response.sounds || [];
+ for (var ii = 0; ii < sounds.length; ii++) {
+ JX.Sound.queue(sounds[ii]);
+ }
+
list.unlock();
},
updateCard: function(response, options) {
options = options || {};
options.dirtyColumns = options.dirtyColumns || {};
var columns = this.getColumns();
var phid = response.objectPHID;
- if (!this._templates[phid]) {
- for (var add_phid in response.columnMaps) {
- var target_column = this.getColumn(add_phid);
+ for (var add_phid in response.columnMaps) {
+ var target_column = this.getColumn(add_phid);
- if (!target_column) {
- // If the column isn't visible, don't try to add a card to it.
- continue;
- }
-
- target_column.newCard(phid);
+ if (!target_column) {
+ // If the column isn't visible, don't try to add a card to it.
+ continue;
}
- }
-
- this.setCardTemplate(phid, response.cardHTML);
- var order_maps = response.orderMaps;
- for (var order_phid in order_maps) {
- this.setOrderMap(order_phid, order_maps[order_phid]);
+ target_column.newCard(phid);
}
var column_maps = response.columnMaps;
var natural_column;
for (var natural_phid in column_maps) {
natural_column = this.getColumn(natural_phid);
if (!natural_column) {
// Our view of the board may be out of date, so we might get back
// information about columns that aren't visible. Just ignore the
// position information for any columns we aren't displaying on the
// client.
continue;
}
natural_column.setNaturalOrder(column_maps[natural_phid]);
}
- var property_maps = response.propertyMaps;
- for (var property_phid in property_maps) {
- this.setObjectProperties(property_phid, property_maps[property_phid]);
+ for (var card_phid in response.cards) {
+ var card_data = response.cards[card_phid];
+ var card_template = this.getCardTemplate(card_phid);
+
+ if (card_data.nodeHTMLTemplate) {
+ card_template.setNodeHTMLTemplate(card_data.nodeHTMLTemplate);
+ }
+
+ var order;
+ for (order in card_data.vectors) {
+ card_template.setSortVector(order, card_data.vectors[order]);
+ }
+
+ for (order in card_data.headers) {
+ card_template.setHeaderKey(order, card_data.headers[order]);
+ }
+
+ for (var key in card_data.properties) {
+ card_template.setObjectProperty(key, card_data.properties[key]);
+ }
+ }
+
+ var headers = response.headers;
+ for (var jj = 0; jj < headers.length; jj++) {
+ var header = headers[jj];
+
+ this.getHeaderTemplate(header.key)
+ .setOrder(header.order)
+ .setNodeHTMLTemplate(header.template)
+ .setVector(header.vector)
+ .setEditProperties(header.editProperties);
}
for (var column_phid in columns) {
var column = columns[column_phid];
var cards = column.getCards();
for (var object_phid in cards) {
if (object_phid !== phid) {
continue;
}
var card = cards[object_phid];
card.redraw();
column.markForRedraw();
}
}
this._redrawColumns();
},
_redrawColumns: function() {
var columns = this.getColumns();
for (var k in columns) {
if (columns[k].isMarkedForRedraw()) {
columns[k].redraw();
}
}
}
}
});
diff --git a/webroot/rsrc/js/application/projects/WorkboardCard.js b/webroot/rsrc/js/application/projects/WorkboardCard.js
index b506e655c..4a3be2a51 100644
--- a/webroot/rsrc/js/application/projects/WorkboardCard.js
+++ b/webroot/rsrc/js/application/projects/WorkboardCard.js
@@ -1,68 +1,79 @@
/**
* @provides javelin-workboard-card
* @requires javelin-install
* @javelin
*/
JX.install('WorkboardCard', {
construct: function(column, phid) {
this._column = column;
this._phid = phid;
},
members: {
_column: null,
_phid: null,
_root: null,
getPHID: function() {
return this._phid;
},
getColumn: function() {
return this._column;
},
setColumn: function(column) {
this._column = column;
},
getProperties: function() {
- return this.getColumn().getBoard().getObjectProperties(this.getPHID());
+ return this.getColumn().getBoard()
+ .getCardTemplate(this.getPHID())
+ .getObjectProperties();
},
getPoints: function() {
return this.getProperties().points;
},
getStatus: function() {
return this.getProperties().status;
},
getNode: function() {
if (!this._root) {
var phid = this.getPHID();
- var template = this.getColumn().getBoard().getCardTemplate(phid);
- this._root = JX.$H(template).getFragment().firstChild;
- JX.Stratcom.getData(this._root).objectPHID = this.getPHID();
+ var root = this.getColumn().getBoard()
+ .getCardTemplate(phid)
+ .newNode();
+
+ JX.Stratcom.getData(root).objectPHID = phid;
+
+ this._root = root;
}
+
return this._root;
},
+ isWorkboardHeader: function() {
+ return false;
+ },
+
redraw: function() {
var old_node = this._root;
this._root = null;
var new_node = this.getNode();
if (old_node && old_node.parentNode) {
JX.DOM.replace(old_node, new_node);
}
return this;
}
}
});
diff --git a/webroot/rsrc/js/application/projects/WorkboardCardTemplate.js b/webroot/rsrc/js/application/projects/WorkboardCardTemplate.js
new file mode 100644
index 000000000..58f3f9e97
--- /dev/null
+++ b/webroot/rsrc/js/application/projects/WorkboardCardTemplate.js
@@ -0,0 +1,64 @@
+/**
+ * @provides javelin-workboard-card-template
+ * @requires javelin-install
+ * @javelin
+ */
+
+JX.install('WorkboardCardTemplate', {
+
+ construct: function(phid) {
+ this._phid = phid;
+ this._vectors = {};
+ this._headerKeys = {};
+
+ this.setObjectProperties({});
+ },
+
+ properties: {
+ objectProperties: null
+ },
+
+ members: {
+ _phid: null,
+ _html: null,
+ _vectors: null,
+ _headerKeys: null,
+
+ getPHID: function() {
+ return this._phid;
+ },
+
+ setNodeHTMLTemplate: function(html) {
+ this._html = html;
+ return this;
+ },
+
+ setSortVector: function(order, vector) {
+ this._vectors[order] = vector;
+ return this;
+ },
+
+ getSortVector: function(order) {
+ return this._vectors[order];
+ },
+
+ setHeaderKey: function(order, key) {
+ this._headerKeys[order] = key;
+ return this;
+ },
+
+ getHeaderKey: function(order) {
+ return this._headerKeys[order];
+ },
+
+ newNode: function() {
+ return JX.$H(this._html).getFragment().firstChild;
+ },
+
+ setObjectProperty: function(key, value) {
+ this.getObjectProperties()[key] = value;
+ return this;
+ }
+ }
+
+});
diff --git a/webroot/rsrc/js/application/projects/WorkboardColumn.js b/webroot/rsrc/js/application/projects/WorkboardColumn.js
index 997364859..a9bf0f8cc 100644
--- a/webroot/rsrc/js/application/projects/WorkboardColumn.js
+++ b/webroot/rsrc/js/application/projects/WorkboardColumn.js
@@ -1,302 +1,486 @@
/**
* @provides javelin-workboard-column
* @requires javelin-install
* javelin-workboard-card
+ * javelin-workboard-header
* @javelin
*/
JX.install('WorkboardColumn', {
construct: function(board, phid, root) {
this._board = board;
this._phid = phid;
this._root = root;
this._panel = JX.DOM.findAbove(root, 'div', 'workpanel');
this._pointsNode = JX.DOM.find(this._panel, 'span', 'column-points');
this._pointsContentNode = JX.DOM.find(
this._panel,
'span',
'column-points-content');
this._cards = {};
+ this._headers = {};
+ this._objects = [];
this._naturalOrder = [];
+ this._dropEffects = [];
+ },
+
+ properties: {
+ triggerPreviewEffect: null
},
members: {
_phid: null,
_root: null,
_board: null,
_cards: null,
+ _headers: null,
_naturalOrder: null,
+ _orderVectors: null,
_panel: null,
_pointsNode: null,
_pointsContentNode: null,
_dirty: true,
+ _objects: null,
+ _dropEffects: null,
getPHID: function() {
return this._phid;
},
getRoot: function() {
return this._root;
},
getCards: function() {
return this._cards;
},
+ _getObjects: function() {
+ return this._objects;
+ },
+
getCard: function(phid) {
return this._cards[phid];
},
getBoard: function() {
return this._board;
},
setNaturalOrder: function(order) {
this._naturalOrder = order;
+ this._orderVectors = null;
return this;
},
+ setDropEffects: function(effects) {
+ this._dropEffects = effects;
+ return this;
+ },
+
+ getDropEffects: function() {
+ return this._dropEffects;
+ },
+
getPointsNode: function() {
return this._pointsNode;
},
getPointsContentNode: function() {
return this._pointsContentNode;
},
getWorkpanelNode: function() {
return this._panel;
},
newCard: function(phid) {
var card = new JX.WorkboardCard(this, phid);
this._cards[phid] = card;
this._naturalOrder.push(phid);
+ this._orderVectors = null;
return card;
},
removeCard: function(phid) {
var card = this._cards[phid];
delete this._cards[phid];
for (var ii = 0; ii < this._naturalOrder.length; ii++) {
if (this._naturalOrder[ii] == phid) {
this._naturalOrder.splice(ii, 1);
+ this._orderVectors = null;
break;
}
}
return card;
},
addCard: function(card, after) {
var phid = card.getPHID();
card.setColumn(this);
this._cards[phid] = card;
var index = 0;
if (after) {
for (var ii = 0; ii < this._naturalOrder.length; ii++) {
if (this._naturalOrder[ii] == after) {
index = ii + 1;
break;
}
}
}
if (index > this._naturalOrder.length) {
this._naturalOrder.push(phid);
} else {
this._naturalOrder.splice(index, 0, phid);
}
+ this._orderVectors = null;
+
return this;
},
- getCardNodes: function() {
- var cards = this.getCards();
+ getDropTargetNodes: function() {
+ var objects = this._getObjects();
var nodes = [];
- for (var k in cards) {
- nodes.push(cards[k].getNode());
+ for (var ii = 0; ii < objects.length; ii++) {
+ var object = objects[ii];
+ nodes.push(object.getNode());
}
return nodes;
},
getCardPHIDs: function() {
return JX.keys(this.getCards());
},
getPointLimit: function() {
return JX.Stratcom.getData(this.getRoot()).pointLimit;
},
markForRedraw: function() {
this._dirty = true;
},
isMarkedForRedraw: function() {
return this._dirty;
},
+ getHeader: function(key) {
+ if (!this._headers[key]) {
+ this._headers[key] = new JX.WorkboardHeader(this, key);
+ }
+ return this._headers[key];
+ },
+
+ handleDragGhost: function(default_handler, ghost, node) {
+ // If the column has headers, don't let the user drag a card above
+ // the topmost header: for example, you can't change a task to have
+ // a priority higher than the highest possible priority.
+
+ if (this._hasColumnHeaders()) {
+ if (!node) {
+ return false;
+ }
+ }
+
+ return default_handler(ghost, node);
+ },
+
+ _hasColumnHeaders: function() {
+ var board = this.getBoard();
+ var order = board.getOrder();
+
+ return board.getOrderTemplate(order).getHasHeaders();
+ },
+
redraw: function() {
var board = this.getBoard();
var order = board.getOrder();
- var list;
- if (order == 'natural') {
- list = this._getCardsSortedNaturally();
- } else {
- list = this._getCardsSortedByKey(order);
+ var list = this._getCardsSortedByKey(order);
+
+ var ii;
+ var objects = [];
+
+ var has_headers = this._hasColumnHeaders();
+ var header_keys = [];
+ var seen_headers = {};
+ if (has_headers) {
+ var header_templates = board.getHeaderTemplatesForOrder(order);
+ for (var k in header_templates) {
+ header_keys.push(header_templates[k].getHeaderKey());
+ }
+ header_keys.reverse();
}
- var content = [];
- for (var ii = 0; ii < list.length; ii++) {
+ var header_key;
+ var next;
+ for (ii = 0; ii < list.length; ii++) {
var card = list[ii];
- var node = card.getNode();
- content.push(node);
+ // If a column has a "High" priority card and a "Low" priority card,
+ // we need to add the "Normal" header in between them. This allows
+ // you to change priority to "Normal" even if there are no "Normal"
+ // cards in a column.
+
+ if (has_headers) {
+ header_key = board.getCardTemplate(card.getPHID())
+ .getHeaderKey(order);
+
+ if (!seen_headers[header_key]) {
+ while (header_keys.length) {
+ next = header_keys.pop();
+
+ var header = this.getHeader(next);
+ objects.push(header);
+ seen_headers[header_key] = true;
+
+ if (next === header_key) {
+ break;
+ }
+ }
+ }
+ }
+
+ objects.push(card);
+ }
+
+ // Add any leftover headers at the bottom of the column which don't have
+ // any cards in them. In particular, empty columns don't have any cards
+ // but should still have headers.
+
+ while (header_keys.length) {
+ next = header_keys.pop();
+ if (seen_headers[next]) {
+ continue;
+ }
+
+ objects.push(this.getHeader(next));
+ }
+
+ this._objects = objects;
+
+ var content = [];
+ for (ii = 0; ii < this._objects.length; ii++) {
+ var object = this._objects[ii];
+
+ var node = object.getNode();
+ content.push(node);
}
JX.DOM.setContent(this.getRoot(), content);
this._redrawFrame();
this._dirty = false;
},
- _getCardsSortedNaturally: function() {
- var list = [];
+ compareHandler: function(src_list, src_node, dst_list, dst_node) {
+ var board = this.getBoard();
+ var order = board.getOrder();
- for (var ii = 0; ii < this._naturalOrder.length; ii++) {
- var phid = this._naturalOrder[ii];
- list.push(this.getCard(phid));
+ var u_vec = this._getNodeOrderVector(src_node, order);
+ var v_vec = this._getNodeOrderVector(dst_node, order);
+
+ return board.compareVectors(u_vec, v_vec);
+ },
+
+ _getNodeOrderVector: function(node, order) {
+ var board = this.getBoard();
+ var data = JX.Stratcom.getData(node);
+
+ if (data.objectPHID) {
+ return this._getOrderVector(data.objectPHID, order);
}
- return list;
+ return board.getHeaderTemplate(data.headerKey).getVector();
+ },
+
+ setIsDropTarget: function(is_target) {
+ var node = this.getWorkpanelNode();
+ JX.DOM.alterClass(node, 'workboard-column-drop-target', is_target);
},
_getCardsSortedByKey: function(order) {
var cards = this.getCards();
var list = [];
for (var k in cards) {
list.push(cards[k]);
}
list.sort(JX.bind(this, this._sortCards, order));
return list;
},
_sortCards: function(order, u, v) {
- var ud = this.getBoard().getOrderVector(u.getPHID(), order);
- var vd = this.getBoard().getOrderVector(v.getPHID(), order);
+ var board = this.getBoard();
+ var u_vec = this._getOrderVector(u.getPHID(), order);
+ var v_vec = this._getOrderVector(v.getPHID(), order);
- for (var ii = 0; ii < ud.length; ii++) {
- if (ud[ii] > vd[ii]) {
- return 1;
+ return board.compareVectors(u_vec, v_vec);
+ },
+
+ _getOrderVector: function(phid, order) {
+ var board = this.getBoard();
+
+ if (!this._orderVectors) {
+ this._orderVectors = {};
+ }
+
+ if (!this._orderVectors[order]) {
+ var cards = this.getCards();
+ var vectors = {};
+
+ for (var k in cards) {
+ var card_phid = cards[k].getPHID();
+ var vector = board.getCardTemplate(card_phid)
+ .getSortVector(order);
+
+ vectors[card_phid] = [].concat(vector);
+
+ // Push a "card" type, so cards always sort after headers; headers
+ // have a "0" in this position.
+ vectors[card_phid].push(1);
}
- if (ud[ii] < vd[ii]) {
- return -1;
+ for (var ii = 0; ii < this._naturalOrder.length; ii++) {
+ var natural_phid = this._naturalOrder[ii];
+ if (vectors[natural_phid]) {
+ vectors[natural_phid].push(ii);
+ }
}
+
+ this._orderVectors[order] = vectors;
+ }
+
+ if (!this._orderVectors[order][phid]) {
+ // In this case, we're comparing a card being dragged in from another
+ // column to the cards already in this column. We're just going to
+ // build a temporary vector for it.
+ var incoming_vector = board.getCardTemplate(phid)
+ .getSortVector(order);
+ incoming_vector = [].concat(incoming_vector);
+
+ // Add a "card" type to sort this after headers.
+ incoming_vector.push(1);
+
+ // Add a "0" for the natural ordering to put this on top. A new card
+ // has no natural ordering on a column it isn't part of yet.
+ incoming_vector.push(0);
+
+ return incoming_vector;
}
- return 0;
+ return this._orderVectors[order][phid];
},
_redrawFrame: function() {
var cards = this.getCards();
var board = this.getBoard();
var points = {};
var count = 0;
var decimal_places = 0;
for (var phid in cards) {
var card = cards[phid];
var card_points;
if (board.getPointsEnabled()) {
card_points = card.getPoints();
} else {
card_points = 1;
}
if (card_points !== null) {
var status = card.getStatus();
if (!points[status]) {
points[status] = 0;
}
points[status] += card_points;
// Count the number of decimal places in the point value with the
// most decimal digits. We'll use the same precision when rendering
// the point sum. This avoids rounding errors and makes the display
// a little more consistent.
var parts = card_points.toString().split('.');
if (parts[1]) {
decimal_places = Math.max(decimal_places, parts[1].length);
}
}
count++;
}
var total_points = 0;
for (var k in points) {
total_points += points[k];
}
total_points = total_points.toFixed(decimal_places);
var limit = this.getPointLimit();
var display_value;
if (limit !== null && limit !== 0) {
display_value = total_points + ' / ' + limit;
} else {
display_value = total_points;
}
if (board.getPointsEnabled()) {
display_value = count + ' | ' + display_value;
}
var over_limit = ((limit !== null) && (total_points > limit));
var content_node = this.getPointsContentNode();
var points_node = this.getPointsNode();
JX.DOM.setContent(content_node, display_value);
- var is_empty = !this.getCardPHIDs().length;
+ // Only put the "empty" style on the column (which just adds some empty
+ // space so it's easier to drop cards into an empty column) if it has no
+ // cards and no headers.
+
+ var is_empty =
+ (!this.getCardPHIDs().length) &&
+ (!this._hasColumnHeaders());
+
var panel = JX.DOM.findAbove(this.getRoot(), 'div', 'workpanel');
JX.DOM.alterClass(panel, 'project-panel-empty', is_empty);
+
+
JX.DOM.alterClass(panel, 'project-panel-over-limit', over_limit);
var color_map = {
'phui-tag-disabled': (total_points === 0),
'phui-tag-blue': (total_points > 0 && !over_limit),
'phui-tag-red': (over_limit)
};
for (var c in color_map) {
JX.DOM.alterClass(points_node, c, !!color_map[c]);
}
JX.DOM.show(points_node);
}
}
});
diff --git a/webroot/rsrc/js/application/projects/WorkboardDropEffect.js b/webroot/rsrc/js/application/projects/WorkboardDropEffect.js
new file mode 100644
index 000000000..0c729fc51
--- /dev/null
+++ b/webroot/rsrc/js/application/projects/WorkboardDropEffect.js
@@ -0,0 +1,73 @@
+/**
+ * @provides javelin-workboard-drop-effect
+ * @requires javelin-install
+ * javelin-dom
+ * @javelin
+ */
+
+JX.install('WorkboardDropEffect', {
+
+ properties: {
+ icon: null,
+ color: null,
+ content: null,
+ isTriggerEffect: false,
+ isHeader: false,
+ conditions: []
+ },
+
+ statics: {
+ newFromDictionary: function(map) {
+ return new JX.WorkboardDropEffect()
+ .setIcon(map.icon)
+ .setColor(map.color)
+ .setContent(JX.$H(map.content))
+ .setIsTriggerEffect(map.isTriggerEffect)
+ .setIsHeader(map.isHeader)
+ .setConditions(map.conditions || []);
+ }
+ },
+
+ members: {
+ newNode: function() {
+ var icon = new JX.PHUIXIconView()
+ .setIcon(this.getIcon())
+ .setColor(this.getColor())
+ .getNode();
+
+ var attributes = {};
+
+ if (this.getIsHeader()) {
+ attributes.className = 'workboard-drop-preview-header';
+ }
+
+ return JX.$N('li', attributes, [icon, this.getContent()]);
+ },
+
+ isEffectVisibleForCard: function(card) {
+ var conditions = this.getConditions();
+
+ var properties = card.getProperties();
+ for (var ii = 0; ii < conditions.length; ii++) {
+ var condition = conditions[ii];
+
+ var field = properties[condition.field];
+ var value = condition.value;
+
+ var result = true;
+ switch (condition.operator) {
+ case '!=':
+ result = (field !== value);
+ break;
+ }
+
+ if (!result) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ }
+});
diff --git a/webroot/rsrc/js/application/projects/WorkboardHeader.js b/webroot/rsrc/js/application/projects/WorkboardHeader.js
new file mode 100644
index 000000000..a0cbfc13c
--- /dev/null
+++ b/webroot/rsrc/js/application/projects/WorkboardHeader.js
@@ -0,0 +1,48 @@
+/**
+ * @provides javelin-workboard-header
+ * @requires javelin-install
+ * @javelin
+ */
+
+JX.install('WorkboardHeader', {
+
+ construct: function(column, header_key) {
+ this._column = column;
+ this._headerKey = header_key;
+ },
+
+ members: {
+ _root: null,
+ _column: null,
+ _headerKey: null,
+
+ getColumn: function() {
+ return this._column;
+ },
+
+ getHeaderKey: function() {
+ return this._headerKey;
+ },
+
+ getNode: function() {
+ if (!this._root) {
+ var header_key = this.getHeaderKey();
+
+ var root = this.getColumn().getBoard()
+ .getHeaderTemplate(header_key)
+ .newNode();
+
+ JX.Stratcom.getData(root).headerKey = header_key;
+
+ this._root = root;
+ }
+
+ return this._root;
+ },
+
+ isWorkboardHeader: function() {
+ return true;
+ }
+ }
+
+});
diff --git a/webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js b/webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js
new file mode 100644
index 000000000..d64a56dd2
--- /dev/null
+++ b/webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js
@@ -0,0 +1,40 @@
+/**
+ * @provides javelin-workboard-header-template
+ * @requires javelin-install
+ * @javelin
+ */
+
+JX.install('WorkboardHeaderTemplate', {
+
+ construct: function(header_key) {
+ this._headerKey = header_key;
+ },
+
+ properties: {
+ template: null,
+ order: null,
+ vector: null,
+ editProperties: null,
+ dropEffects: []
+ },
+
+ members: {
+ _headerKey: null,
+ _html: null,
+
+ getHeaderKey: function() {
+ return this._headerKey;
+ },
+
+ setNodeHTMLTemplate: function(html) {
+ this._html = html;
+ return this;
+ },
+
+ newNode: function() {
+ return JX.$H(this._html).getFragment().firstChild;
+ }
+
+ }
+
+});
diff --git a/webroot/rsrc/js/application/projects/WorkboardOrderTemplate.js b/webroot/rsrc/js/application/projects/WorkboardOrderTemplate.js
new file mode 100644
index 000000000..083dc78b5
--- /dev/null
+++ b/webroot/rsrc/js/application/projects/WorkboardOrderTemplate.js
@@ -0,0 +1,27 @@
+/**
+ * @provides javelin-workboard-order-template
+ * @requires javelin-install
+ * @javelin
+ */
+
+JX.install('WorkboardOrderTemplate', {
+
+ construct: function(order) {
+ this._orderKey = order;
+ },
+
+ properties: {
+ hasHeaders: false,
+ canReorder: false
+ },
+
+ members: {
+ _orderKey: null,
+
+ getOrderKey: function() {
+ return this._orderKey;
+ }
+
+ }
+
+});
diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js
index 83f41787a..bba6db7a4 100644
--- a/webroot/rsrc/js/application/projects/behavior-project-boards.js
+++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js
@@ -1,110 +1,181 @@
/**
* @provides javelin-behavior-project-boards
* @requires javelin-behavior
* javelin-dom
* javelin-util
* javelin-vector
* javelin-stratcom
* javelin-workflow
* javelin-workboard-controller
+ * javelin-workboard-drop-effect
*/
JX.behavior('project-boards', function(config, statics) {
function update_statics(update_config) {
statics.boardID = update_config.boardID;
statics.projectPHID = update_config.projectPHID;
statics.order = update_config.order;
statics.moveURI = update_config.moveURI;
}
function setup() {
JX.Stratcom.listen('click', 'boards-dropdown-menu', function(e) {
var data = e.getNodeData('boards-dropdown-menu');
if (data.menu) {
return;
}
e.kill();
var list = JX.$H(data.items).getFragment().firstChild;
var button = e.getNode('boards-dropdown-menu');
data.menu = new JX.PHUIXDropdownMenu(button);
data.menu.setContent(list);
data.menu.open();
});
JX.Stratcom.listen(
'quicksand-redraw',
null,
function (e) {
var data = e.getData();
if (!data.newResponse.boardConfig) {
return;
}
var new_config;
if (data.fromServer) {
new_config = data.newResponse.boardConfig;
statics.boardConfigCache[data.newResponseID] = new_config;
} else {
new_config = statics.boardConfigCache[data.newResponseID];
statics.boardID = new_config.boardID;
}
update_statics(new_config);
});
return true;
}
if (!statics.setup) {
update_statics(config);
var current_page_id = JX.Quicksand.getCurrentPageID();
statics.boardConfigCache = {};
statics.boardConfigCache[current_page_id] = config;
statics.setup = setup();
}
if (!statics.workboard) {
statics.workboard = new JX.WorkboardController()
.setUploadURI(config.uploadURI)
.setCoverURI(config.coverURI)
.setMoveURI(config.moveURI)
.setChunkThreshold(config.chunkThreshold)
.start();
}
var board_phid = config.projectPHID;
var board_node = JX.$(config.boardID);
var board = statics.workboard.newBoard(board_phid, board_node)
.setOrder(config.order)
.setPointsEnabled(config.pointsEnabled);
var templates = config.templateMap;
for (var k in templates) {
- board.setCardTemplate(k, templates[k]);
+ board.getCardTemplate(k)
+ .setNodeHTMLTemplate(templates[k]);
}
- var column_maps = config.columnMaps;
- for (var column_phid in column_maps) {
- var column = board.getColumn(column_phid);
- var column_map = column_maps[column_phid];
- for (var ii = 0; ii < column_map.length; ii++) {
- column.newCard(column_map[ii]);
+ var ii;
+ var jj;
+ var effects;
+
+ for (ii = 0; ii < config.columnTemplates.length; ii++) {
+ var spec = config.columnTemplates[ii];
+
+ var column = board.getColumn(spec.columnPHID);
+
+ effects = [];
+ for (jj = 0; jj < spec.effects.length; jj++) {
+ effects.push(
+ JX.WorkboardDropEffect.newFromDictionary(
+ spec.effects[jj]));
+ }
+ column.setDropEffects(effects);
+
+ for (jj = 0; jj < spec.cardPHIDs.length; jj++) {
+ column.newCard(spec.cardPHIDs[jj]);
+ }
+
+ if (spec.triggerPreviewEffect) {
+ column.setTriggerPreviewEffect(
+ JX.WorkboardDropEffect.newFromDictionary(
+ spec.triggerPreviewEffect));
}
}
var order_maps = config.orderMaps;
for (var object_phid in order_maps) {
- board.setOrderMap(object_phid, order_maps[object_phid]);
+ var order_card = board.getCardTemplate(object_phid);
+ for (var order_key in order_maps[object_phid]) {
+ order_card.setSortVector(order_key, order_maps[object_phid][order_key]);
+ }
}
var property_maps = config.propertyMaps;
for (var property_phid in property_maps) {
- board.setObjectProperties(property_phid, property_maps[property_phid]);
+ board.getCardTemplate(property_phid)
+ .setObjectProperties(property_maps[property_phid]);
+ }
+
+ var headers = config.headers;
+ for (ii = 0; ii < headers.length; ii++) {
+ var header = headers[ii];
+
+ effects = [];
+ for (jj = 0; jj < header.effects.length; jj++) {
+ effects.push(
+ JX.WorkboardDropEffect.newFromDictionary(
+ header.effects[jj]));
+ }
+
+ board.getHeaderTemplate(header.key)
+ .setOrder(header.order)
+ .setNodeHTMLTemplate(header.template)
+ .setVector(header.vector)
+ .setEditProperties(header.editProperties)
+ .setDropEffects(effects);
+ }
+
+ var orders = config.orders;
+ for (ii = 0; ii < orders.length; ii++) {
+ var order = orders[ii];
+
+ board.getOrderTemplate(order.orderKey)
+ .setHasHeaders(order.hasHeaders)
+ .setCanReorder(order.canReorder);
+ }
+
+ var header_keys = config.headerKeys;
+ for (var header_phid in header_keys) {
+ board.getCardTemplate(header_phid)
+ .setHeaderKey(config.order, header_keys[header_phid]);
}
board.start();
+ // In Safari, we can only play sounds that we've already loaded, and we can
+ // only load them in response to an explicit user interaction like a click.
+ var sounds = config.preloadSounds;
+ var listener = JX.Stratcom.listen('mousedown', null, function() {
+ for (var ii = 0; ii < sounds.length; ii++) {
+ JX.Sound.load(sounds[ii]);
+ }
+
+ // Remove this callback once it has run once.
+ listener.remove();
+ });
+
});
diff --git a/webroot/rsrc/js/application/repository/repository-crossreference.js b/webroot/rsrc/js/application/repository/repository-crossreference.js
index 548ef6173..d6ff2a06a 100644
--- a/webroot/rsrc/js/application/repository/repository-crossreference.js
+++ b/webroot/rsrc/js/application/repository/repository-crossreference.js
@@ -1,355 +1,350 @@
/**
* @provides javelin-behavior-repository-crossreference
* @requires javelin-behavior
* javelin-dom
* javelin-stratcom
* javelin-uri
*/
JX.behavior('repository-crossreference', function(config, statics) {
var highlighted;
var linked = [];
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';
var classMouseCursor = 'crossreference-cursor';
// TODO maybe move the dictionary part of this list to the server?
var class_map = {
nc : 'class',
nf : 'function',
na : null,
nb : 'builtin',
n : null
};
function link(element, lang) {
JX.DOM.alterClass(element, 'repository-crossreference', true);
linked.push(element);
JX.DOM.listen(
element,
['mouseover', 'mouseout', 'click'],
'tag:span',
function(e) {
if (e.getType() === '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 "<span />" 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 (e.getType() === 'mouseover') {
while (target && target !== document.body) {
if (JX.DOM.isNode(target, 'span') &&
(target.className in class_map)) {
highlighted = target;
JX.DOM.alterClass(highlighted, classHighlight, true);
break;
}
target = target.parentNode;
}
} else if (e.getType() === 'click') {
openSearch(target, {lang: lang});
}
});
}
function unhighlight() {
highlighted && JX.DOM.alterClass(highlighted, classHighlight, false);
highlighted = null;
}
function openSearch(target, context) {
var symbol = target.textContent || target.innerText;
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();
if (class_map[c]) {
query.type = class_map[c];
}
if (target.hasAttribute('data-symbol-context')) {
query.context = target.getAttribute('data-symbol-context');
}
if (target.hasAttribute('data-symbol-name')) {
symbol = target.getAttribute('data-symbol-name');
}
var line = getLineNumber(target);
if (line !== null) {
query.line = line;
}
if (!query.hasOwnProperty('path')) {
var path = getPath(target);
if (path !== null) {
query.path = path;
}
}
var char = getChar(target);
if (char !== null) {
query.char = char;
}
var uri = JX.$U('/diffusion/symbol/' + symbol + '/');
uri.addQueryParams(query);
window.open(uri.toString());
}
function linkAll() {
var blocks = JX.DOM.scry(document.body, 'div', 'remarkup-code-block');
for (var i = 0; i < blocks.length; ++i) {
if (blocks[i].hasAttribute('data-code-lang')) {
var lang = blocks[i].getAttribute('data-code-lang');
link(blocks[i], lang);
}
}
}
function getLineNumber(target) {
// Figure out the line number by finding the most recent "<th />" in this
// row with a number in it. We may need to skip over one "<th />" if the
// diff is being displayed in unified mode.
var cell = JX.DOM.findAbove(target, 'td');
if (!cell) {
return null;
}
var row = JX.DOM.findAbove(target, 'tr');
if (!row) {
return null;
}
var ii;
var cell_list = [];
for (ii = 0; ii < row.childNodes.length; ii++) {
cell_list.push(row.childNodes[ii]);
}
cell_list.reverse();
var found = false;
for (ii = 0; ii < cell_list.length; ii++) {
if (cell_list[ii] === cell) {
found = true;
}
if (found && JX.DOM.isType(cell_list[ii], 'th')) {
var int_value = parseInt(cell_list[ii].textContent, 10);
if (int_value) {
return int_value;
}
}
}
return null;
}
function getPath(target) {
// This method works in Differential, when browsing a changset.
var changeset;
try {
changeset = JX.DOM.findAbove(target, 'div', 'differential-changeset');
return JX.Stratcom.getData(changeset).path;
} catch (ex) {
// Ignore.
}
return null;
}
function getChar(target) {
var cell = JX.DOM.findAbove(target, 'td');
if (!cell) {
return null;
}
var char = 1;
for (var ii = 0; ii < cell.childNodes.length; ii++) {
var node = cell.childNodes[ii];
if (node === target) {
return char;
}
var content = '' + node.textContent;
-
- // Strip off any ZWS characters. These are marker characters used to
- // improve copy/paste behavior.
- content = content.replace(/\u200B/g, '');
-
char += content.length;
}
return null;
}
JX.Stratcom.listen(
'differential-preview-update',
null,
function(e) {
linkAll(e.getData().container);
});
JX.Stratcom.listen(
['keydown', 'keyup'],
null,
function(e) {
if (!isHighlightModifierKey(e)) {
return;
}
setCursorMode(e.getType() === 'keydown');
if (!statics.active) {
unhighlight();
}
});
JX.Stratcom.listen(
'blur',
null,
function(e) {
if (e.getTarget()) {
return;
}
unhighlight();
setCursorMode(false);
});
function setCursorMode(active) {
statics.active = active;
linked.forEach(function(element) {
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 "<span />" 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/application/trigger/TriggerRule.js b/webroot/rsrc/js/application/trigger/TriggerRule.js
new file mode 100644
index 000000000..cf117e24d
--- /dev/null
+++ b/webroot/rsrc/js/application/trigger/TriggerRule.js
@@ -0,0 +1,138 @@
+/**
+ * @provides trigger-rule
+ * @javelin
+ */
+
+JX.install('TriggerRule', {
+
+ construct: function() {
+ },
+
+ properties: {
+ rowID: null,
+ type: null,
+ value: null,
+ editor: null,
+ isValidRule: true,
+ invalidView: null
+ },
+
+ statics: {
+ newFromDictionary: function(map) {
+ return new JX.TriggerRule()
+ .setType(map.type)
+ .setValue(map.value)
+ .setIsValidRule(map.isValidRule)
+ .setInvalidView(map.invalidView);
+ },
+ },
+
+ members: {
+ _typeCell: null,
+ _valueCell: null,
+ _readValueCallback: null,
+
+ newRowContent: function() {
+ if (!this.getIsValidRule()) {
+ var invalid_cell = JX.$N(
+ 'td',
+ {
+ colSpan: 2,
+ className: 'invalid-cell'
+ },
+ JX.$H(this.getInvalidView()));
+
+ return [invalid_cell];
+ }
+
+ var type_cell = this._getTypeCell();
+ var value_cell = this._getValueCell();
+
+
+ this._rebuildValueControl();
+
+ return [type_cell, value_cell];
+ },
+
+ getValueForSubmit: function() {
+ this._readValueFromControl();
+
+ return {
+ type: this.getType(),
+ value: this.getValue()
+ };
+ },
+
+ _getTypeCell: function() {
+ if (!this._typeCell) {
+ var editor = this.getEditor();
+ var types = editor.getTypes();
+
+ var options = [];
+ for (var ii = 0; ii < types.length; ii++) {
+ var type = types[ii];
+
+ if (!type.getIsSelectable()) {
+ continue;
+ }
+
+ options.push(
+ JX.$N('option', {value: type.getType()}, type.getName()));
+ }
+
+ var control = JX.$N('select', {}, options);
+
+ control.value = this.getType();
+
+ var on_change = JX.bind(this, this._onTypeChange, control);
+ JX.DOM.listen(control, 'change', null, on_change);
+
+ var attributes = {
+ className: 'type-cell'
+ };
+
+ this._typeCell = JX.$N('td', attributes, control);
+ }
+
+ return this._typeCell;
+ },
+
+ _onTypeChange: function(control) {
+ this.setType(control.value);
+ this._rebuildValueControl();
+ },
+
+ _getValueCell: function() {
+ if (!this._valueCell) {
+ var attributes = {
+ className: 'value-cell'
+ };
+
+ this._valueCell = JX.$N('td', attributes);
+ }
+
+ return this._valueCell;
+ },
+
+ _rebuildValueControl: function() {
+ var value_cell = this._getValueCell();
+
+ var editor = this.getEditor();
+ var type = editor.getType(this.getType());
+ var control = type.getControl();
+
+ var input = control.newInput(this);
+ this._readValueCallback = input.get;
+
+ JX.DOM.setContent(value_cell, input.node);
+ },
+
+ _readValueFromControl: function() {
+ if (this._readValueCallback) {
+ this.setValue(this._readValueCallback());
+ }
+ }
+
+ }
+
+});
diff --git a/webroot/rsrc/js/application/trigger/TriggerRuleControl.js b/webroot/rsrc/js/application/trigger/TriggerRuleControl.js
new file mode 100644
index 000000000..a05e740ff
--- /dev/null
+++ b/webroot/rsrc/js/application/trigger/TriggerRuleControl.js
@@ -0,0 +1,40 @@
+/**
+ * @requires phuix-form-control-view
+ * @provides trigger-rule-control
+ * @javelin
+ */
+
+JX.install('TriggerRuleControl', {
+
+ construct: function() {
+ },
+
+ properties: {
+ type: null,
+ specification: null
+ },
+
+ statics: {
+ newFromDictionary: function(map) {
+ return new JX.TriggerRuleControl()
+ .setType(map.type)
+ .setSpecification(map.specification);
+ },
+ },
+
+ members: {
+ newInput: function(rule) {
+ var phuix = new JX.PHUIXFormControl()
+ .setControl(this.getType(), this.getSpecification());
+
+ phuix.setValue(rule.getValue());
+
+ return {
+ node: phuix.getRawInputNode(),
+ get: JX.bind(phuix, phuix.getValue)
+ };
+ }
+
+ }
+
+});
diff --git a/webroot/rsrc/js/application/trigger/TriggerRuleEditor.js b/webroot/rsrc/js/application/trigger/TriggerRuleEditor.js
new file mode 100644
index 000000000..3574a8dbc
--- /dev/null
+++ b/webroot/rsrc/js/application/trigger/TriggerRuleEditor.js
@@ -0,0 +1,137 @@
+/**
+ * @requires multirow-row-manager
+ * trigger-rule
+ * @provides trigger-rule-editor
+ * @javelin
+ */
+
+JX.install('TriggerRuleEditor', {
+
+ construct: function(form_node) {
+ this._formNode = form_node;
+ this._rules = [];
+ this._types = [];
+ },
+
+ members: {
+ _formNode: null,
+ _tableNode: null,
+ _createButtonNode: null,
+ _inputNode: null,
+ _rowManager: null,
+ _rules: null,
+ _types: null,
+
+ setTableNode: function(table) {
+ this._tableNode = table;
+ return this;
+ },
+
+ setCreateButtonNode: function(button) {
+ this._createButtonNode = button;
+ return this;
+ },
+
+ setInputNode: function(input) {
+ this._inputNode = input;
+ return this;
+ },
+
+ start: function() {
+ var on_submit = JX.bind(this, this._submitForm);
+ JX.DOM.listen(this._formNode, 'submit', null, on_submit);
+
+ var manager = new JX.MultirowRowManager(this._tableNode);
+ this._rowManager = manager;
+
+ var on_remove = JX.bind(this, this._rowRemoved);
+ manager.listen('row-removed', on_remove);
+
+ var create_button = this._createButtonNode;
+ var on_create = JX.bind(this, this._createRow);
+ JX.DOM.listen(create_button, 'click', null, on_create);
+ },
+
+ _submitForm: function() {
+ var values = [];
+ for (var ii = 0; ii < this._rules.length; ii++) {
+ var rule = this._rules[ii];
+ values.push(rule.getValueForSubmit());
+ }
+
+ this._inputNode.value = JX.JSON.stringify(values);
+ },
+
+ _createRow: function(e) {
+ var rule = this.newRule();
+ this.addRule(rule);
+ e.kill();
+ },
+
+ newRule: function() {
+ // Create new rules with the first valid rule type.
+ var types = this.getTypes();
+ var type;
+ for (var ii = 0; ii < types.length; ii++) {
+ type = types[ii];
+ if (!type.getIsSelectable()) {
+ continue;
+ }
+
+ // If we make it here: this type is valid, so use it.
+ break;
+ }
+
+ var default_value = type.getDefaultValue();
+
+ return new JX.TriggerRule()
+ .setType(type.getType())
+ .setValue(default_value);
+ },
+
+ addRule: function(rule) {
+ rule.setEditor(this);
+ this._rules.push(rule);
+
+ var manager = this._rowManager;
+
+ var row = manager.addRow([]);
+ var row_id = manager.getRowID(row);
+ rule.setRowID(row_id);
+
+ manager.updateRow(row_id, rule.newRowContent());
+ },
+
+ addType: function(type) {
+ this._types.push(type);
+ return this;
+ },
+
+ getTypes: function() {
+ return this._types;
+ },
+
+ getType: function(type) {
+ for (var ii = 0; ii < this._types.length; ii++) {
+ if (this._types[ii].getType() === type) {
+ return this._types[ii];
+ }
+ }
+
+ return null;
+ },
+
+ _rowRemoved: function(row_id) {
+ for (var ii = 0; ii < this._rules.length; ii++) {
+ var rule = this._rules[ii];
+
+ if (rule.getRowID() === row_id) {
+ this._rules.splice(ii, 1);
+ break;
+ }
+ }
+ }
+
+ }
+
+});
diff --git a/webroot/rsrc/js/application/trigger/TriggerRuleType.js b/webroot/rsrc/js/application/trigger/TriggerRuleType.js
new file mode 100644
index 000000000..1075eeced
--- /dev/null
+++ b/webroot/rsrc/js/application/trigger/TriggerRuleType.js
@@ -0,0 +1,36 @@
+/**
+ * @requires trigger-rule-control
+ * @provides trigger-rule-type
+ * @javelin
+ */
+
+JX.install('TriggerRuleType', {
+
+ construct: function() {
+ },
+
+ properties: {
+ type: null,
+ name: null,
+ isSelectable: true,
+ defaultValue: null,
+ control: null
+ },
+
+ statics: {
+ newFromDictionary: function(map) {
+ var control = JX.TriggerRuleControl.newFromDictionary(map.control);
+
+ return new JX.TriggerRuleType()
+ .setType(map.type)
+ .setName(map.name)
+ .setIsSelectable(map.selectable)
+ .setDefaultValue(map.defaultValue)
+ .setControl(control);
+ },
+ },
+
+ members: {
+ }
+
+});
diff --git a/webroot/rsrc/js/application/trigger/trigger-rule-editor.js b/webroot/rsrc/js/application/trigger/trigger-rule-editor.js
new file mode 100644
index 000000000..d2741cc33
--- /dev/null
+++ b/webroot/rsrc/js/application/trigger/trigger-rule-editor.js
@@ -0,0 +1,41 @@
+/**
+ * @requires javelin-behavior
+ * trigger-rule-editor
+ * trigger-rule
+ * trigger-rule-type
+ * @provides javelin-behavior-trigger-rule-editor
+ * @javelin
+ */
+
+JX.behavior('trigger-rule-editor', function(config) {
+ var form_node = JX.$(config.formNodeID);
+ var table_node = JX.$(config.tableNodeID);
+ var create_node = JX.$(config.createNodeID);
+ var input_node = JX.$(config.inputNodeID);
+
+ var editor = new JX.TriggerRuleEditor(form_node)
+ .setTableNode(table_node)
+ .setCreateButtonNode(create_node)
+ .setInputNode(input_node);
+
+ editor.start();
+
+ var ii;
+
+ for (ii = 0; ii < config.types.length; ii++) {
+ var type = JX.TriggerRuleType.newFromDictionary(config.types[ii]);
+ editor.addType(type);
+ }
+
+ if (config.rules.length) {
+ for (ii = 0; ii < config.rules.length; ii++) {
+ var rule = JX.TriggerRule.newFromDictionary(config.rules[ii]);
+ editor.addRule(rule);
+ }
+ } else {
+ // If the trigger doesn't have any rules yet, add an empty rule to start
+ // with, so the user doesn't have to click "New Rule".
+ editor.addRule(editor.newRule());
+ }
+
+});
diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js
index a545ed727..5f19b7061 100644
--- a/webroot/rsrc/js/core/DraggableList.js
+++ b/webroot/rsrc/js/core/DraggableList.js
@@ -1,758 +1,841 @@
/**
* @provides phabricator-draggable-list
* @requires javelin-install
* javelin-dom
* javelin-stratcom
* javelin-util
* javelin-vector
* javelin-magical-init
* @javelin
*/
JX.install('DraggableList', {
construct : function(sigil, root) {
this._sigil = sigil;
this._root = root || document.body;
this._group = [this];
// NOTE: Javelin does not dispatch mousemove by default.
JX.enableDispatch(document.body, 'mousemove');
JX.DOM.listen(this._root, 'mousedown', sigil, JX.bind(this, this._ondrag));
JX.Stratcom.listen('mousemove', null, JX.bind(this, this._onmove));
JX.Stratcom.listen('scroll', null, JX.bind(this, this._onmove));
JX.Stratcom.listen('mouseup', null, JX.bind(this, this._ondrop));
JX.Stratcom.listen('keypress', null, JX.bind(this, this._onkey));
},
events : [
'didLock',
'didUnlock',
'shouldBeginDrag',
'didBeginDrag',
'didCancelDrag',
'didEndDrag',
'didDrop',
'didSend',
'didReceive'],
properties : {
findItemsHandler: null,
+ compareHandler: null,
+ isDropTargetHandler: null,
canDragX: false,
outerContainer: null,
- hasInfiniteHeight: false
+ hasInfiniteHeight: false,
+ compareOnMove: false,
+ compareOnReorder: false,
+ targetChangeHandler: null
},
members : {
_root : null,
_dragging : null,
_locked : 0,
_target : null,
+ _lastTarget: null,
_targets : null,
_ghostHandler : null,
_ghostNode : null,
_group : null,
_cursorPosition: null,
_cursorOrigin: null,
_cursorScroll: null,
_frame: null,
_clone: null,
_offset: null,
_autoscroll: null,
_autoscroller: null,
_autotimer: null,
getRootNode : function() {
return this._root;
},
setGhostHandler : function(handler) {
this._ghostHandler = handler;
return this;
},
getGhostHandler : function() {
return this._ghostHandler || JX.bind(this, this._defaultGhostHandler);
},
getGhostNode : function() {
if (!this._ghostNode) {
this._ghostNode = JX.$N('li', {className: 'drag-ghost'});
}
return this._ghostNode;
},
setGhostNode : function(node) {
this._ghostNode = node;
return this;
},
setGroup : function(lists) {
var result = [];
var need_self = true;
for (var ii = 0; ii < lists.length; ii++) {
if (lists[ii] == this) {
need_self = false;
}
result.push(lists[ii]);
}
if (need_self) {
result.push(this);
}
this._group = result;
return this;
},
_hasGroup : function() {
return (this._group.length > 1);
},
_defaultGhostHandler : function(ghost, target) {
var parent;
if (!this._hasGroup()) {
parent = this._dragging.parentNode;
} else {
parent = this.getRootNode();
}
if (target && target.nextSibling) {
parent.insertBefore(ghost, target.nextSibling);
} else if (!target && parent.firstChild) {
parent.insertBefore(ghost, parent.firstChild);
} else {
parent.appendChild(ghost);
}
},
findItems : function() {
var handler = this.getFindItemsHandler();
if (__DEV__) {
if (!handler) {
JX.$E('JX.Draggable.findItems(): No findItemsHandler set!');
}
}
var items = handler();
// Make sure the clone element is never included as a target.
for (var ii = 0; ii < items.length; ii++) {
if (items[ii] === this._clone) {
items.splice(ii, 1);
break;
}
}
return items;
},
_ondrag : function(e) {
if (this._dragging) {
// Don't start dragging if we're already dragging something.
return;
}
if (this._locked) {
// Don't start drag operations while locked.
return;
}
if (!e.isNormalMouseEvent()) {
// Don't start dragging for shift click, right click, etc.
return;
}
if (this.invoke('shouldBeginDrag', e).getPrevented()) {
return;
}
if (e.getNode('tag:a')) {
// Never start a drag if we're somewhere inside an <a> tag. This makes
// links unclickable in Firefox.
return;
}
if (JX.Stratcom.pass()) {
// Let other handlers deal with this event before we do.
return;
}
e.kill();
var drag = e.getNode(this._sigil);
this._autoscroll = {};
this._autoscroller = setInterval(JX.bind(this, this._onautoscroll), 10);
this._autotimer = null;
for (var ii = 0; ii < this._group.length; ii++) {
this._group[ii]._clearTarget();
}
var pos = JX.$V(drag);
var dim = JX.Vector.getDim(drag);
// Create and adjust the ghost nodes.
for (var jj = 0; jj < this._group.length; jj++) {
var ghost = this._group[jj].getGhostNode();
ghost.style.height = dim.y + 'px';
}
// Here's what's going on: we're cloning the thing that's being dragged.
// This is the "clone", stored in "this._clone". We're going to leave the
// original where it is in the document, and put the clone at top-level
// so it can be freely dragged around the whole document, even if it's
// inside a container with overflow hidden.
// Because the clone has been moved up, CSS classes which rely on some
// parent selector won't work. Draggable objects need to pick up all of
// their CSS properties without relying on container classes. This isn't
// great, but leaving them where they are in the document creates a large
// number of positioning problems with scrollable, absolute, relative,
// or overflow hidden containers.
// Note that we don't actually want to let the user drag it outside the
// document. One problem is that doing so lets the user drag objects
// infinitely far to the right by dragging them to the edge so the
// document extends, scrolling the document, dragging them to the edge
// of the new larger document, scrolling the document, and so on forever.
// To prevent this, we're putting a "frame" (stored in "this._frame") at
// top level, then putting the clone inside the frame. The frame has the
// same size as the entire viewport, and overflow hidden, so dragging the
// item outside the document just cuts it off.
// Create the clone for dragging.
var clone = drag.cloneNode(true);
pos.setPos(clone);
dim.setDim(clone);
JX.DOM.alterClass(drag, 'drag-dragging', true);
JX.DOM.alterClass(clone, 'drag-clone', true);
var frame = JX.$N('div', {className: 'drag-frame'});
frame.appendChild(clone);
document.body.appendChild(frame);
+ JX.DOM.alterClass(document.body, 'jx-dragging', true);
this._dragging = drag;
this._clone = clone;
this._frame = frame;
var cursor = JX.$V(e);
this._offset = new JX.Vector(pos.x - cursor.x, pos.y - cursor.y);
JX.Tooltip.lock();
this.invoke('didBeginDrag', this._dragging);
},
_getTargets : function() {
if (this._targets === null) {
var targets = [];
var items = this.findItems();
for (var ii = 0; ii < items.length; ii++) {
var item = items[ii];
var ipos = JX.$V(item);
targets.push({
item: items[ii],
y: ipos.y + (JX.Vector.getDim(items[ii]).y / 2)
});
}
targets.sort(function(u, v) { return v.y - u.y; });
this._targets = targets;
}
return this._targets;
},
_dirtyTargetCache: function() {
if (this._hasGroup()) {
var group = this._group;
for (var ii = 0; ii < group.length; ii++) {
group[ii]._targets = null;
}
} else {
this._targets = null;
}
return this;
},
_getTargetList : function(p) {
var target_list;
var infinity;
if (this._hasGroup()) {
var group = this._group;
for (var ii = 0; ii < group.length; ii++) {
var root = group[ii].getRootNode();
var rp = JX.$V(root);
var rd = JX.Vector.getDim(root);
if (group[ii].getHasInfiniteHeight()) {
// The math doesn't work out quite right if we actually use
// Math.Infinity, so approximate infinity as the larger of the
// document height or viewport height.
if (!infinity) {
infinity = Math.max(
JX.Vector.getViewport().y,
JX.Vector.getDocument().y);
}
rp.y = 0;
rd.y = infinity;
}
var is_target = false;
if (p.x >= rp.x && p.y >= rp.y) {
if (p.x <= (rp.x + rd.x) && p.y <= (rp.y + rd.y)) {
is_target = true;
target_list = group[ii];
}
}
- JX.DOM.alterClass(root, 'drag-target-list', is_target);
+ group[ii]._setIsDropTarget(is_target);
}
} else {
target_list = this;
}
return target_list;
},
_getTarget: function() {
return this._target;
},
_setTarget : function(cur_target) {
var ghost = this.getGhostNode();
var target = this._target;
if (cur_target !== target) {
this._clearTarget();
if (cur_target !== false) {
var ok = this.getGhostHandler()(ghost, cur_target);
// If the handler returns explicit `false`, prevent the drag.
if (ok === false) {
cur_target = false;
}
}
this._target = cur_target;
}
return this;
},
_clearTarget : function() {
var target = this._target;
var ghost = this.getGhostNode();
if (target !== false) {
JX.DOM.remove(ghost);
}
this._target = false;
// Clear the target position cache, since adding or removing ghosts
// changes element positions.
this._dirtyTargetCache();
return this;
},
+ _didChangeTarget: function(dst_list, dst_node) {
+ if (dst_node === this._lastTarget) {
+ return;
+ }
+
+ this._lastTarget = dst_node;
+
+ var handler = this.getTargetChangeHandler();
+ if (handler) {
+ handler(this, this._dragging, dst_list, dst_node);
+ }
+ },
+
+ _setIsDropTarget: function(is_target) {
+ var root = this.getRootNode();
+ JX.DOM.alterClass(root, 'drag-target-list', is_target);
+
+ var handler = this.getIsDropTargetHandler();
+ if (handler) {
+ handler(is_target);
+ }
+
+ return this;
+ },
+
+ _getOrderedTarget: function(src_list, src_node) {
+ var targets = this._getTargets();
+
+ // NOTE: The targets are ordered from the bottom of the column to the
+ // top, so we're looking for the first node that we sort below. If we
+ // don't find one, we'll sort to the head of the column.
+
+ for (var ii = 0; ii < targets.length; ii++) {
+ var target = targets[ii];
+ if (this._compareTargets(src_list, src_node, target.item) > 0) {
+ return target.item;
+ }
+ }
+
+ return null;
+ },
+
+ _compareTargets: function(src_list, src_node, dst_node) {
+ var dst_list = this;
+ return this.getCompareHandler()(src_list, src_node, dst_list, dst_node);
+ },
+
_getCurrentTarget : function(p) {
- var ghost = this.getGhostNode();
var targets = this._getTargets();
var dragging = this._dragging;
// Find the node we're dragging the object underneath. This is the first
// node in the list that's above the cursor. If that node is the node
// we're dragging or its predecessor, don't select a target, because the
// operation would be a no-op.
// NOTE: When we're dragging into the first position in the list, we
// use the target `null`. When we don't have a valid target, we use
// the target `false`. Spooky! Magic! Anyway, `null` and `false` mean
// completely different things.
var cur_target = null;
var trigger;
for (var ii = 0; ii < targets.length; ii++) {
trigger = targets[ii].y;
// If the cursor is above this target, we aren't dropping underneath it.
if (trigger >= p.y) {
continue;
}
// Don't choose the dragged row or its predecessor as targets.
cur_target = targets[ii].item;
if (!dragging) {
// If the item on the cursor isn't from this list, it can't be
// dropped onto itself or its predecessor in this list.
} else {
if (cur_target === dragging) {
cur_target = false;
}
if (targets[ii - 1] && (targets[ii - 1].item === dragging)) {
cur_target = false;
}
}
break;
}
// If the dragged row is the first row, don't allow it to be dragged
// into the first position, since this operation doesn't make sense.
if (dragging && cur_target === null) {
var first_item = targets[targets.length - 1].item;
if (dragging === first_item) {
cur_target = false;
}
}
return cur_target;
},
_onmove : function(e) {
// We'll get a callback here for "mousemove" (and can determine the
// location of the cursor) and also for "scroll" (and can not). If this
// is a move, save the mouse position, so if we get a scroll next we can
// reuse the known position.
if (e.getType() == 'mousemove') {
this._cursorPosition = JX.$V(e);
this._cursorOrigin = JX.$V(e);
this._cursorScroll = JX.Vector.getScroll();
}
if (!this._dragging) {
return;
}
if (!this._cursorPosition) {
return;
}
if (e.getType() == 'scroll') {
// If this is a scroll event, the positions of drag targets may have
// changed.
this._dirtyTargetCache();
// Correct the cursor position to account for scrolling.
var s = JX.Vector.getScroll();
this._cursorPosition = new JX.$V(
this._cursorOrigin.x - (this._cursorScroll.x - s.x),
this._cursorOrigin.y - (this._cursorScroll.y - s.y));
}
var p = JX.$V(this._cursorPosition.x, this._cursorPosition.y);
var group = this._group;
var target_list = this._getTargetList(p);
// Compute the size and position of the drop target indicator, because we
// need to update our static position computations to account for it.
+ var compare_handler = this.getCompareHandler();
+
var cur_target = false;
if (target_list) {
- cur_target = target_list._getCurrentTarget(p);
+ // Determine if we're going to use the compare handler or not: the
+ // compare hander locks items into a specific place in the list. For
+ // example, on Workboards, some operations permit the user to drag
+ // items between lists, but not to reorder items within a list.
+
+ var should_compare = false;
+
+ var is_reorder = (target_list === this);
+ var is_move = (target_list !== this);
+
+ if (compare_handler) {
+ if (is_reorder && this.getCompareOnReorder()) {
+ should_compare = true;
+ }
+ if (is_move && this.getCompareOnMove()) {
+ should_compare = true;
+ }
+ }
+
+ if (should_compare) {
+ cur_target = target_list._getOrderedTarget(this, this._dragging);
+ } else {
+ cur_target = target_list._getCurrentTarget(p);
+ }
}
// If we've selected a new target, update the UI to show where we're
// going to drop the row.
for (var ii = 0; ii < group.length; ii++) {
if (group[ii] == target_list) {
group[ii]._setTarget(cur_target);
} else {
group[ii]._clearTarget();
}
}
+ this._didChangeTarget(target_list, cur_target);
+
this._updateAutoscroll(this._cursorPosition);
var f = JX.$V(this._frame);
p.x -= f.x;
p.y -= f.y;
p.y += this._offset.y;
this._clone.style.top = p.y + 'px';
if (this.getCanDragX()) {
p.x += this._offset.x;
this._clone.style.left = p.x + 'px';
}
e.kill();
},
_updateAutoscroll: function(p) {
var container = this._getScrollAnchor().parentNode;
var autoscroll = {};
var outer = this.getOuterContainer();
var cpos;
var cdim;
while (container) {
if (outer && (container == outer)) {
break;
}
try {
cpos = JX.Vector.getPos(container);
cdim = JX.Vector.getDim(container);
if (container == document.body) {
cdim = JX.Vector.getViewport();
cpos.x += container.scrollLeft;
cpos.y += container.scrollTop;
}
} catch (ignored) {
break;
}
var fuzz = 64;
if (p.y <= cpos.y + fuzz) {
autoscroll.up = container;
}
if (p.y >= cpos.y + cdim.y - fuzz) {
autoscroll.down = container;
}
if (p.x <= cpos.x + fuzz) {
autoscroll.left = container;
}
if (p.x >= cpos.x + cdim.x - fuzz) {
autoscroll.right = container;
}
if (container == document.body) {
break;
}
container = container.parentNode;
}
this._autoscroll = autoscroll;
},
_onkey: function(e) {
// Cancel any current drag if the user presses escape.
if (this._dragging && (e.getSpecialKey() == 'esc')) {
e.kill();
this._drop(null);
return;
}
},
_ondrop : function(e) {
if (this._dragging) {
e.kill();
}
var p = JX.$V(e);
this._drop(p);
},
_drop: function(cursor) {
if (!this._dragging) {
return;
}
var dragging = this._dragging;
this._dragging = null;
clearInterval(this._autoscroller);
this._autoscroller = null;
JX.DOM.remove(this._frame);
+ JX.DOM.alterClass(document.body, 'jx-dragging', false);
this._frame = null;
this._clone = null;
var target = false;
var ghost = false;
if (cursor) {
var target_list = this._getTargetList(cursor);
if (target_list) {
target = target_list._target;
ghost = target_list.getGhostNode();
}
}
JX.$V(0, 0).setPos(dragging);
if (target === false) {
this.invoke('didCancelDrag', dragging);
} else {
JX.DOM.remove(dragging);
JX.DOM.replace(ghost, dragging);
this.invoke('didSend', dragging, target_list);
target_list.invoke('didReceive', dragging, this);
target_list.invoke('didDrop', dragging, target, this);
}
var group = this._group;
for (var ii = 0; ii < group.length; ii++) {
- JX.DOM.alterClass(group[ii].getRootNode(), 'drag-target-list', false);
+ group[ii]._setIsDropTarget(false);
group[ii]._clearTarget();
}
+ this._didChangeTarget(null, null);
+
JX.DOM.alterClass(dragging, 'drag-dragging', false);
JX.Tooltip.unlock();
this.invoke('didEndDrag', dragging);
},
_getScrollAnchor: function() {
// If you drag an item from column "A" into column "B", then move the
// mouse to the top or bottom of the screen, we need to scroll the target
// column (column "B"), not the original column.
var group = this._group;
for (var ii = 0; ii < group.length; ii++) {
var target = group[ii]._getTarget();
if (target) {
return group[ii]._ghostNode;
}
}
return this._dragging;
},
_onautoscroll: function() {
var u = this._autoscroll.up;
var d = this._autoscroll.down;
var l = this._autoscroll.left;
var r = this._autoscroll.right;
var now = +new Date();
if (!this._autotimer) {
this._autotimer = now;
return;
}
var delta = now - this._autotimer;
this._autotimer = now;
var amount = 12 * (delta / 10);
var anchor = this._getScrollAnchor();
if (u && (u != d)) {
this._tryScroll(anchor, u, 'scrollTop', amount);
}
if (d && (d != u)) {
this._tryScroll(anchor, d, 'scrollTop', -amount);
}
if (l && (l != r)) {
this._tryScroll(anchor, l, 'scrollLeft', amount);
}
if (r && (r != l)) {
this._tryScroll(anchor, r, 'scrollLeft', -amount);
}
},
/**
* Walk up the tree from a node to some parent, trying to scroll every
* container. Stop when we find a container which we're able to scroll.
*/
_tryScroll: function(from, to, property, amount) {
var value;
var container = from.parentNode;
while (container) {
// In Safari, we'll eventually reach `window.document`, which is not
// sufficently node-like to support sigil tests.
var lock = false;
if (container === window.document) {
lock = false;
} else {
// Some elements may respond to, e.g., `scrollTop` adjustment, even
// though they are not scrollable. This sigil disables adjustment
// for them.
var lock_sigil;
if (property == 'scrollTop') {
lock_sigil = 'lock-scroll-y-while-dragging';
}
if (lock_sigil) {
lock = JX.Stratcom.hasSigil(container, lock_sigil);
}
}
if (!lock) {
// Read the current scroll value.
value = container[property];
// Try to scroll.
container[property] -= amount;
// If we scrolled it, we're all done.
if (container[property] != value) {
break;
}
if (container == to) {
break;
}
}
container = container.parentNode;
}
},
lock : function() {
for (var ii = 0; ii < this._group.length; ii++) {
this._group[ii]._lock();
}
return this;
},
_lock : function() {
this._locked++;
if (this._locked === 1) {
this.invoke('didLock');
}
return this;
},
unlock: function() {
for (var ii = 0; ii < this._group.length; ii++) {
this._group[ii]._unlock();
}
return this;
},
_unlock : function() {
if (__DEV__) {
if (!this._locked) {
JX.$E('JX.Draggable.unlock(): Draggable is not locked!');
}
}
this._locked--;
if (!this._locked) {
this.invoke('didUnlock');
}
return this;
}
}
});
diff --git a/webroot/rsrc/js/core/Prefab.js b/webroot/rsrc/js/core/Prefab.js
index 979ad3473..ff4467881 100644
--- a/webroot/rsrc/js/core/Prefab.js
+++ b/webroot/rsrc/js/core/Prefab.js
@@ -1,315 +1,338 @@
/**
* @provides phabricator-prefab
* @requires javelin-install
* javelin-util
* javelin-dom
* javelin-typeahead
* javelin-tokenizer
* javelin-typeahead-preloaded-source
* javelin-typeahead-ondemand-source
* javelin-dom
* javelin-stratcom
* javelin-util
* @javelin
*/
/**
* Utilities for client-side rendering (the greatest thing in the world).
*/
JX.install('Prefab', {
statics : {
renderSelect : function(map, selected, attrs, order) {
var select = JX.$N('select', attrs || {});
// Callers may optionally pass "order" to force options into a specific
// order. Although most browsers do retain order, maps in Javascript
// aren't technically ordered. Safari, at least, will reorder maps with
// numeric keys.
order = order || JX.keys(map);
var k;
for (var ii = 0; ii < order.length; ii++) {
k = order[ii];
select.options[select.options.length] = new Option(map[k], k);
if (k == selected) {
select.value = k;
}
}
select.value = select.value || order[0];
return select;
},
newTokenizerFromTemplate: function(markup, config) {
var template = JX.$H(markup).getFragment().firstChild;
var container = JX.DOM.find(template, 'div', 'tokenizer-container');
container.id = '';
config.root = container;
var build = JX.Prefab.buildTokenizer(config);
build.node = template;
return build;
},
/**
* Build a Phabricator tokenizer out of a configuration with application
* sorting, datasource and placeholder rules.
*
* - `id` Root tokenizer ID (alternatively, pass `root`).
* - `root` Root tokenizer node (replaces `id`).
* - `src` Datasource URI.
* - `ondemand` Optional, use an ondemand source.
* - `value` Optional, initial value.
* - `limit` Optional, token limit.
* - `placeholder` Optional, placeholder text.
* - `username` Optional, username to sort first (i.e., viewer).
* - `icons` Optional, map of icons.
*
*/
buildTokenizer : function(config) {
config.icons = config.icons || {};
var root;
try {
root = config.root || JX.$(config.id);
} catch (ex) {
// If the root element does not exist, just return without building
// anything. This happens in some cases -- like Conpherence -- where we
// may load a tokenizer but not put it in the document.
return;
}
var datasource;
// Default to an ondemand source if no alternate configuration is
// provided.
var ondemand = true;
if ('ondemand' in config) {
ondemand = config.ondemand;
}
if (ondemand) {
datasource = new JX.TypeaheadOnDemandSource(config.src);
} else {
datasource = new JX.TypeaheadPreloadedSource(config.src);
}
datasource.setSortHandler(
JX.bind(datasource, JX.Prefab.sortHandler, config));
datasource.setTransformer(JX.Prefab.transformDatasourceResults);
var typeahead = new JX.Typeahead(
root,
JX.DOM.find(root, 'input', 'tokenizer-input'));
typeahead.setDatasource(datasource);
var tokenizer = new JX.Tokenizer(root);
tokenizer.setTypeahead(typeahead);
tokenizer.setRenderTokenCallback(function(value, key, container) {
var result;
if (value && (typeof value == 'object') && ('id' in value)) {
// TODO: In this case, we've been passed the decoded wire format
// dictionary directly. Token rendering is kind of a huge mess that
// should be cleaned up and made more consistent. Just force our
// way through for now.
result = value;
} else {
result = datasource.getResult(key);
}
var icon;
var type;
var color;
+ var availability_color;
if (result) {
icon = result.icon;
value = result.displayName;
type = result.tokenType;
color = result.color;
+ availability_color = result.availabilityColor;
} else {
icon = (config.icons || {})[key];
type = (config.types || {})[key];
color = (config.colors || {})[key];
+ availability_color = (config.availabilityColors || {})[key];
}
if (icon) {
icon = JX.Prefab._renderIcon(icon);
}
type = type || 'object';
JX.DOM.alterClass(container, 'jx-tokenizer-token-' + type, true);
if (color) {
JX.DOM.alterClass(container, color, true);
}
- return [icon, value];
+ var dot;
+ if (availability_color) {
+ dot = JX.$N(
+ 'span',
+ {
+ className: 'phui-tag-dot phui-tag-color-' + availability_color
+ });
+ }
+
+ return [icon, dot, value];
});
if (config.placeholder) {
tokenizer.setPlaceholder(config.placeholder);
}
if (config.limit) {
tokenizer.setLimit(config.limit);
}
if (config.value) {
tokenizer.setInitialValue(config.value);
}
if (config.browseURI) {
tokenizer.setBrowseURI(config.browseURI);
}
if (config.disabled) {
tokenizer.setDisabled(true);
}
JX.Stratcom.addData(root, {'tokenizer' : tokenizer});
return {
tokenizer: tokenizer
};
},
sortHandler: function(config, value, list, cmp) {
// Sort results so that the viewing user always comes up first; after
// that, prefer unixname matches to realname matches.
var priority_hits = {};
var self_hits = {};
// We'll put matches where the user's input is a prefix of the name
// above matches where that isn't true.
var prefix_hits = {};
var tokens = this.tokenize(value);
var normal = this.normalize(value);
for (var ii = 0; ii < list.length; ii++) {
var item = list[ii];
if (this.normalize(item.name).indexOf(normal) === 0) {
prefix_hits[item.id] = true;
}
if (!item.priority) {
continue;
}
if (config.username && item.priority == config.username) {
self_hits[item.id] = true;
}
}
list.sort(function(u, v) {
if (self_hits[u.id] != self_hits[v.id]) {
return self_hits[v.id] ? 1 : -1;
}
// If one result is open and one is closed, show the open result
// first. The "!" tricks here are because closed values are display
// strings, so the value is either `null` or some truthy string. If
// we compare the values directly, we'll apply this rule to two
// objects which are both closed but for different reasons, like
// "Archived" and "Disabled".
var u_open = !u.closed;
var v_open = !v.closed;
if (u_open != v_open) {
if (u_open) {
return -1;
} else {
return 1;
}
}
if (prefix_hits[u.id] != prefix_hits[v.id]) {
return prefix_hits[v.id] ? 1 : -1;
}
// Sort users ahead of other result types.
if (u.priorityType != v.priorityType) {
if (u.priorityType == 'user') {
return -1;
}
if (v.priorityType == 'user') {
return 1;
}
}
// Sort functions after other result types.
var uf = (u.tokenType == 'function');
var vf = (v.tokenType == 'function');
if (uf != vf) {
return uf ? 1 : -1;
}
return cmp(u, v);
});
},
/**
* Transform results from a wire format into a usable format in a standard
* way.
*/
transformDatasourceResults: function(fields) {
var closed = fields[9];
var closed_ui;
if (closed) {
closed_ui = JX.$N(
'div',
{className: 'tokenizer-closed'},
closed);
}
var icon = fields[8];
var icon_ui;
if (icon) {
icon_ui = JX.Prefab._renderIcon(icon);
}
+ var availability_ui;
+ var availability_color = fields[16];
+ if (availability_color) {
+ availability_ui = JX.$N(
+ 'span',
+ {
+ className: 'phui-tag-dot phui-tag-color-' + availability_color
+ });
+ }
+
var display = JX.$N(
'div',
{className: 'tokenizer-result'},
- [icon_ui, fields[4] || fields[0], closed_ui]);
+ [icon_ui, availability_ui, fields[4] || fields[0], closed_ui]);
if (closed) {
JX.DOM.alterClass(display, 'tokenizer-result-closed', true);
}
return {
name: fields[0],
displayName: fields[4] || fields[0],
display: display,
uri: fields[1],
id: fields[2],
priority: fields[3],
priorityType: fields[7],
imageURI: fields[6],
icon: icon,
closed: closed,
type: fields[5],
sprite: fields[10],
color: fields[11],
tokenType: fields[12],
unique: fields[13] || false,
autocomplete: fields[14],
- sort: JX.TypeaheadNormalizer.normalize(fields[0])
+ sort: JX.TypeaheadNormalizer.normalize(fields[0]),
+ availabilityColor: availability_color
};
},
_renderIcon: function(icon) {
return JX.$N(
'span',
{className: 'phui-icon-view phui-font-fa ' + icon});
}
}
});
diff --git a/webroot/rsrc/js/core/behavior-oncopy.js b/webroot/rsrc/js/core/behavior-oncopy.js
index aa8c684fe..b56e83ab3 100644
--- a/webroot/rsrc/js/core/behavior-oncopy.js
+++ b/webroot/rsrc/js/core/behavior-oncopy.js
@@ -1,65 +1,322 @@
/**
* @provides javelin-behavior-phabricator-oncopy
* @requires javelin-behavior
* javelin-dom
*/
-/**
- * Tools like Paste and Differential don't normally respond to the clipboard
- * 'copy' operation well, because when a user copies text they'll get line
- * numbers and other metadata.
- *
- * To improve this behavior, applications can embed markers that delimit
- * metadata (left of the marker) from content (right of the marker). When
- * we get a copy event, we strip out all the metadata and just copy the
- * actual text.
- */
JX.behavior('phabricator-oncopy', function() {
+ var copy_root;
+ var copy_mode;
- var zws = '\u200B'; // Unicode Zero-Width Space
+ function onstartselect(e) {
+ var target = e.getTarget();
- JX.enableDispatch(document.body, 'copy');
- JX.Stratcom.listen(
- ['copy'],
- null,
- function(e) {
-
- var selection;
- var text;
- if (window.getSelection) {
- selection = window.getSelection();
- text = selection.toString();
+ var container;
+ try {
+ // NOTE: For now, all elements with custom oncopy behavior are tables,
+ // so this tag selection will hit everything we need it to.
+ container = JX.DOM.findAbove(target, 'table', 'intercept-copy');
+ } catch (ex) {
+ container = null;
+ }
+
+ var old_mode = copy_mode;
+ clear_selection_mode();
+
+ if (!container) {
+ return;
+ }
+
+ // If the potential selection is starting inside an inline comment,
+ // don't do anything special.
+ try {
+ if (JX.DOM.findAbove(target, 'div', 'differential-inline-comment')) {
+ return;
+ }
+ } catch (ex) {
+ // Continue.
+ }
+
+ // Find the row and cell we're copying from. If we don't find anything,
+ // don't do anything special.
+ var row;
+ var cell;
+ try {
+ // The target may be the cell we're after, particularly if you click
+ // in the white area to the right of the text, towards the end of a line.
+ if (JX.DOM.isType(target, 'td')) {
+ cell = target;
} else {
- selection = document.selection;
- text = selection.createRange().text;
+ cell = JX.DOM.findAbove(target, 'td');
+ }
+ row = JX.DOM.findAbove(target, 'tr');
+ } catch (ex) {
+ return;
+ }
+
+ // If the row doesn't have enough nodes, bail out. Note that it's okay
+ // to begin a selection in the whitespace on the opposite side of an inline
+ // comment. For example, if there's an inline comment on the right side of
+ // a diff, it's okay to start selecting the left side of the diff by
+ // clicking the corresponding empty space on the left side.
+ if (row.childNodes.length < 4) {
+ return;
+ }
+
+ // If the selection's cell is in the "old" diff or the "new" diff, we'll
+ // activate an appropriate copy mode.
+ var mode;
+ if (cell === row.childNodes[1]) {
+ mode = 'copy-l';
+ } else if ((row.childNodes.length >= 4) && (cell === row.childNodes[4])) {
+ mode = 'copy-r';
+ } else {
+ return;
+ }
+
+ // We found a copy mode, so set it as the current active mode.
+ copy_root = container;
+ copy_mode = mode;
+
+ // If the user makes a selection, then clicks again inside the same
+ // selection, browsers retain the selection. This is because the user may
+ // want to drag-and-drop the text to another window.
+
+ // Handle special cases when the click is inside an existing selection.
+
+ var ranges = get_selected_ranges();
+ if (ranges.length) {
+ // We'll have an existing selection if the user selects text on the right
+ // side of a diff, then clicks the selection on the left side of the
+ // diff, even if the second click is clicking part of the selection
+ // range where the selection highlight is currently invisible because
+ // of CSS rules.
+
+ // This behavior looks and feels glitchy: an invisible selection range
+ // suddenly pops into existence and there's a bunch of flicker. If we're
+ // switching selection modes, clear the old selection to avoid this:
+ // assume the user is not trying to drag-and-drop text which is not
+ // visually selected.
+
+ if (old_mode !== copy_mode) {
+ window.getSelection().removeAllRanges();
+ }
+
+ // In the more mundane case, if the user selects some text on one side
+ // of a diff and then clicks that same selection in a normal way (in
+ // the visible part of the highlighted text), we may either be altering
+ // the selection range or may be initiating a text drag depending on how
+ // long they hold the button for. Regardless of what we're doing, we're
+ // still in a selection mode, so keep the visual hints active.
+
+ JX.DOM.alterClass(copy_root, copy_mode, true);
+ }
+
+ // We've chosen a mode and saved it now, but we don't actually update to
+ // apply any visual changes until the user actually starts making some
+ // kind of selection.
+ }
+
+ // When the selection range changes, apply CSS classes if the selection is
+ // nonempty. We don't want to make visual changes to the document immediately
+ // when the user press the mouse button, since we aren't yet sure that
+ // they are starting a selection: instead, wait for them to actually select
+ // something.
+ function onchangeselect() {
+ if (!copy_mode) {
+ return;
+ }
+
+ var ranges = get_selected_ranges();
+ JX.DOM.alterClass(copy_root, copy_mode, !!ranges.length);
+ }
+
+ // When the user releases the mouse, get rid of the selection mode if we
+ // don't have a selection.
+ function onendselect(e) {
+ if (!copy_mode) {
+ return;
+ }
+
+ var ranges = get_selected_ranges();
+ if (!ranges.length) {
+ clear_selection_mode();
+ }
+ }
+
+ function get_selected_ranges() {
+ var ranges = [];
+
+ if (!window.getSelection) {
+ return ranges;
+ }
+
+ var selection = window.getSelection();
+ for (var ii = 0; ii < selection.rangeCount; ii++) {
+ var range = selection.getRangeAt(ii);
+ if (range.collapsed) {
+ continue;
+ }
+
+ ranges.push(range);
+ }
+
+ return ranges;
+ }
+
+ function clear_selection_mode() {
+ if (!copy_root) {
+ return;
+ }
+
+ JX.DOM.alterClass(copy_root, copy_mode, false);
+ copy_root = null;
+ copy_mode = null;
+ }
+
+ function oncopy(e) {
+ // If we aren't in a special copy mode, just fall back to default
+ // behavior.
+ if (!copy_mode) {
+ return;
+ }
+
+ var ranges = get_selected_ranges();
+ if (!ranges.length) {
+ return;
+ }
+
+ var text = [];
+ for (var ii = 0; ii < ranges.length; ii++) {
+ var range = ranges[ii];
+
+ var fragment = range.cloneContents();
+ if (!fragment.childNodes.length) {
+ continue;
+ }
+
+ // In Chrome and Firefox, because we've already applied "user-select"
+ // CSS to everything we don't intend to copy, the text in the selection
+ // range is correct, and the range will include only the correct text
+ // nodes.
+
+ // However, in Safari, "user-select" does not apply to clipboard
+ // operations, so we get everything in the document between the beginning
+ // and end of the selection, even if it isn't visibly selected.
+
+ // Even in Chrome and Firefox, we can get partial empty nodes: for
+ // example, where a "<tr>" is selectable but no content in the node is
+ // selectable. (We have to leave the "<tr>" itself selectable because
+ // of how Firefox applies "user-select" rules.)
+
+ // The nodes we get here can also start and end more or less anywhere.
+
+ // One saving grace is that we use "content: attr(data-n);" to render
+ // the line numbers and no browsers copy this content, so we don't have
+ // to worry about figuring out when text is line numbers.
+
+ for (var jj = 0; jj < fragment.childNodes.length; jj++) {
+ var node = fragment.childNodes[jj];
+ text.push(extract_text(node));
+ }
+ }
+
+ text = flatten_list(text);
+ text = text.join('');
+
+ var rawEvent = e.getRawEvent();
+ var data;
+ if ('clipboardData' in rawEvent) {
+ data = rawEvent.clipboardData;
+ } else {
+ data = window.clipboardData;
+ }
+ data.setData('Text', text);
+
+ e.prevent();
+ }
+
+ function extract_text(node) {
+ var ii;
+ var text = [];
+
+ if (JX.DOM.isType(node, 'tr')) {
+ // This is an inline comment row, so we never want to copy any
+ // content inside of it.
+ if (JX.Stratcom.hasSigil(node, 'inline-row')) {
+ return null;
}
- if (text.indexOf(zws) == -1) {
- // If there's no marker in the text, just let it copy normally.
+ // This is a "Show More Context" row, so we never want to copy any
+ // of the content inside.
+ if (JX.Stratcom.hasSigil(node, 'context-target')) {
+ return null;
+ }
+
+ // Assume anything else is a source code row. Keep only "<td>" cells
+ // with the correct mode.
+ for (ii = 0; ii < node.childNodes.length; ii++) {
+ text.push(extract_text(node.childNodes[ii]));
+ }
+
+ return text;
+ }
+
+ if (JX.DOM.isType(node, 'td')) {
+ var node_mode = node.getAttribute('data-copy-mode');
+ if (node_mode !== copy_mode) {
return;
}
- var result = [];
-
- // Strip everything before the marker (and the marker itself) out of the
- // text. If a line doesn't have the marker, throw it away (the assumption
- // is that it's a line number or part of some other meta-text).
- var lines = text.split('\n');
- var pos;
- for (var ii = 0; ii < lines.length; ii++) {
- pos = lines[ii].indexOf(zws);
- if (pos == -1 && ii !== 0) {
- continue;
+ // Otherwise, fall through and extract this node's text normally.
+ }
+
+ if (node.getAttribute) {
+ var copy_text = node.getAttribute('data-copy-text');
+ if (copy_text) {
+ return copy_text;
+ }
+ }
+
+ if (!node.childNodes || !node.childNodes.length) {
+ return node.textContent;
+ }
+
+ for (ii = 0; ii < node.childNodes.length; ii++) {
+ var child = node.childNodes[ii];
+ text.push(extract_text(child));
+ }
+
+ return text;
+ }
+
+ function flatten_list(list) {
+ var stack = [list];
+ var result = [];
+ while (stack.length) {
+ var next = stack.pop();
+ if (JX.isArray(next)) {
+ for (var ii = 0; ii < next.length; ii++) {
+ stack.push(next[ii]);
}
- result.push(lines[ii].substring(pos + 1));
+ } else if (next === null) {
+ continue;
+ } else if (next === undefined) {
+ continue;
+ } else {
+ result.push(next);
}
- result = result.join('\n');
-
- var rawEvent = e.getRawEvent();
- var clipboardData = 'clipboardData' in rawEvent ?
- rawEvent.clipboardData :
- window.clipboardData;
- clipboardData.setData('Text', result);
- e.prevent();
- });
+ }
+
+ return result.reverse();
+ }
+
+ JX.enableDispatch(document.body, 'copy');
+ JX.enableDispatch(window, 'selectionchange');
+
+ JX.Stratcom.listen('mousedown', null, onstartselect);
+ JX.Stratcom.listen('selectionchange', null, onchangeselect);
+ JX.Stratcom.listen('mouseup', null, onendselect);
+
+ JX.Stratcom.listen('copy', null, oncopy);
});
diff --git a/webroot/rsrc/js/core/behavior-toggle-class.js b/webroot/rsrc/js/core/behavior-toggle-class.js
index d4756eb6b..18663b048 100644
--- a/webroot/rsrc/js/core/behavior-toggle-class.js
+++ b/webroot/rsrc/js/core/behavior-toggle-class.js
@@ -1,45 +1,36 @@
/**
* @provides javelin-behavior-toggle-class
* @requires javelin-behavior
* javelin-stratcom
* javelin-dom
*/
/**
* Toggle CSS classes when an element is clicked. This behavior is activated
* by adding the sigil `jx-toggle-class` to an element, and a key `map` to its
* data. The `map` should be a map from element IDs to the classes that should
* be toggled on them.
*
* Optionally, you may provide a `state` key to set the default state of the
* element.
*/
JX.behavior('toggle-class', function(config, statics) {
function install() {
JX.Stratcom.listen(
- ['touchstart', 'mousedown'],
+ 'click',
'jx-toggle-class',
function(e) {
e.kill();
var t = e.getNodeData('jx-toggle-class');
t.state = !t.state;
for (var k in t.map) {
JX.DOM.alterClass(JX.$(k), t.map[k], t.state);
}
});
- // Swallow the regular click handler event so e.g. Quicksand
- // click handler doesn't get a hold of it
- JX.Stratcom.listen(
- ['click'],
- 'jx-toggle-class',
- function(e) {
- e.kill();
- });
-
return true;
}
statics.install = statics.install || install();
});
diff --git a/webroot/rsrc/js/phui/behavior-phui-timer-control.js b/webroot/rsrc/js/phui/behavior-phui-timer-control.js
new file mode 100644
index 000000000..d5b73a5ee
--- /dev/null
+++ b/webroot/rsrc/js/phui/behavior-phui-timer-control.js
@@ -0,0 +1,41 @@
+/**
+ * @provides javelin-behavior-phui-timer-control
+ * @requires javelin-behavior
+ * javelin-stratcom
+ * javelin-dom
+ */
+
+JX.behavior('phui-timer-control', function(config) {
+ var node = JX.$(config.nodeID);
+ var uri = config.uri;
+ var state = null;
+
+ function onupdate(result) {
+ var markup = result.markup;
+ if (markup) {
+ var new_node = JX.$H(markup).getFragment().firstChild;
+ JX.DOM.replace(node, new_node);
+ node = new_node;
+
+ // If the overall state has changed from the previous display state,
+ // animate the control to draw the user's attention to the state change.
+ if (result.state !== state) {
+ state = result.state;
+ JX.DOM.alterClass(node, 'phui-form-timer-updated', true);
+ }
+ }
+
+ var retry = result.retry;
+ if (retry) {
+ setTimeout(update, 1000);
+ }
+ }
+
+ function update() {
+ new JX.Request(uri, onupdate)
+ .setTimeout(10000)
+ .send();
+ }
+
+ update();
+});
diff --git a/webroot/rsrc/js/phuix/PHUIXAutocomplete.js b/webroot/rsrc/js/phuix/PHUIXAutocomplete.js
index f46e7666e..deb9f9d10 100644
--- a/webroot/rsrc/js/phuix/PHUIXAutocomplete.js
+++ b/webroot/rsrc/js/phuix/PHUIXAutocomplete.js
@@ -1,766 +1,775 @@
/**
* @provides phuix-autocomplete
* @requires javelin-install
* javelin-dom
* phuix-icon-view
* phabricator-prefab
*/
JX.install('PHUIXAutocomplete', {
construct: function() {
this._map = {};
this._datasources = {};
this._listNodes = [];
this._resultMap = {};
},
members: {
_area: null,
_active: false,
_cursorHead: null,
_cursorTail: null,
_pixelHead: null,
_pixelTail: null,
_map: null,
_datasource: null,
_datasources: null,
_value: null,
_node: null,
_echoNode: null,
_listNode: null,
_promptNode: null,
_focus: null,
_focusRef: null,
_listNodes: null,
_x: null,
_y: null,
_visible: false,
_resultMap: null,
setArea: function(area) {
this._area = area;
return this;
},
addAutocomplete: function(code, spec) {
this._map[code] = spec;
return this;
},
start: function() {
var area = this._area;
JX.DOM.listen(area, 'keypress', null, JX.bind(this, this._onkeypress));
JX.DOM.listen(
area,
['click', 'keyup', 'keydown', 'keypress'],
null,
JX.bind(this, this._update));
var select = JX.bind(this, this._onselect);
JX.DOM.listen(this._getNode(), 'mousedown', 'typeahead-result', select);
var device = JX.bind(this, this._ondevice);
JX.Stratcom.listen('phabricator-device-change', null, device);
// When the user clicks away from the textarea, deactivate.
var deactivate = JX.bind(this, this._deactivate);
JX.DOM.listen(area, 'blur', null, deactivate);
},
_getSpec: function() {
return this._map[this._active];
},
_ondevice: function() {
if (JX.Device.getDevice() != 'desktop') {
this._deactivate();
}
},
_activate: function(code) {
if (JX.Device.getDevice() != 'desktop') {
return;
}
if (!this._map[code]) {
return;
}
var area = this._area;
var range = JX.TextAreaUtils.getSelectionRange(area);
// Check the character immediately before the trigger character. We'll
// only activate the typeahead if it's something that we think a user
// might reasonably want to autocomplete after, like a space, newline,
// or open parenthesis. For example, if a user types "alincoln@",
// the prior letter will be the last "n" in "alincoln". They are probably
// typing an email address, not a username, so we don't activate the
// autocomplete.
var head = range.start;
var prior;
if (head > 1) {
prior = area.value.substring(head - 2, head - 1);
} else {
prior = '<start>';
}
// If this is a repeating sequence and the previous character is the
// same as the one the user just typed, like "((", don't reactivate.
if (prior === String.fromCharCode(code)) {
return;
}
switch (prior) {
case '<start>':
case ' ':
case '\n':
case '\t':
case '(': // Might be "(@username, what do you think?)".
case '-': // Might be an unnumbered list.
case '.': // Might be a numbered list.
case '|': // Might be a table cell.
case '>': // Might be a blockquote.
case '!': // Might be a blockquote attribution line.
// We'll let these autocomplete.
break;
default:
// We bail out on anything else, since the user is probably not
// typing a username or project tag.
return;
}
// Get all the text on the current line. If the line only contains
// whitespace, don't activate: the user is probably typing code or a
// numbered list.
var line = area.value.substring(0, head - 1);
line = line.split('\n');
line = line[line.length - 1];
if (line.match(/^\s+$/)) {
return;
}
this._cursorHead = head;
this._cursorTail = range.end;
this._pixelHead = JX.TextAreaUtils.getPixelDimensions(
area,
range.start,
range.end);
var spec = this._map[code];
if (!this._datasources[code]) {
var datasource = new JX.TypeaheadOnDemandSource(spec.datasourceURI);
datasource.listen(
'resultsready',
JX.bind(this, this._onresults, code));
datasource.setTransformer(JX.bind(this, this._transformresult));
datasource.setSortHandler(
JX.bind(datasource, JX.Prefab.sortHandler, {}));
this._datasources[code] = datasource;
}
this._datasource = this._datasources[code];
this._active = code;
var head_icon = new JX.PHUIXIconView()
.setIcon(spec.headerIcon)
.getNode();
var head_text = spec.headerText;
var node = this._getPromptNode();
JX.DOM.setContent(node, [head_icon, head_text]);
},
_transformresult: function(fields) {
var map = JX.Prefab.transformDatasourceResults(fields);
var icon;
if (map.icon) {
icon = new JX.PHUIXIconView()
.setIcon(map.icon)
.getNode();
}
- var display = JX.$N('span', {}, [icon, map.displayName]);
+ var dot;
+ if (map.availabilityColor) {
+ dot = JX.$N(
+ 'span',
+ {
+ className: 'phui-tag-dot phui-tag-color-' + map.availabilityColor
+ });
+ }
+
+ var display = JX.$N('span', {}, [icon, dot, map.displayName]);
JX.DOM.alterClass(display, 'tokenizer-result-closed', !!map.closed);
map.display = display;
return map;
},
_deactivate: function() {
var node = this._getNode();
JX.DOM.hide(node);
this._active = false;
this._visible = false;
},
_onkeypress: function(e) {
var r = e.getRawEvent();
// NOTE: We allow events to continue with "altKey", because you need
// to press Alt to type characters like "@" on a German keyboard layout.
// The cost of misfiring autocompleters is very small since we do not
// eat the keystroke. See T10252.
if (r.metaKey || (r.ctrlKey && !r.altKey)) {
return;
}
var code = r.charCode;
if (this._map[code]) {
setTimeout(JX.bind(this, this._activate, code), 0);
}
},
_onresults: function(code, nodes, value, partial) {
// Even if these results are out of date, we still want to fill in the
// result map so we can terminate things later.
if (!partial) {
if (!this._resultMap[code]) {
this._resultMap[code] = {};
}
var hits = [];
for (var ii = 0; ii < nodes.length; ii++) {
var result = this._datasources[code].getResult(nodes[ii].rel);
if (!result) {
hits = null;
break;
}
if (!result.autocomplete || !result.autocomplete.length) {
hits = null;
break;
}
hits.push(result.autocomplete);
}
if (hits !== null) {
this._resultMap[code][value] = hits;
}
}
if (code !== this._active) {
return;
}
if (value !== this._value) {
return;
}
if (this._isTerminatedString(value)) {
if (this._hasUnrefinableResults(value)) {
this._deactivate();
return;
}
}
var list = this._getListNode();
JX.DOM.setContent(list, nodes);
this._listNodes = nodes;
var old_ref = this._focusRef;
this._clearFocus();
for (var ii = 0; ii < nodes.length; ii++) {
if (nodes[ii].rel == old_ref) {
this._setFocus(ii);
break;
}
}
if (this._focus === null && nodes.length) {
this._setFocus(0);
}
this._redraw();
},
_setFocus: function(idx) {
if (!this._listNodes[idx]) {
this._clearFocus();
return false;
}
if (this._focus !== null) {
JX.DOM.alterClass(this._listNodes[this._focus], 'focused', false);
}
this._focus = idx;
this._focusRef = this._listNodes[idx].rel;
JX.DOM.alterClass(this._listNodes[idx], 'focused', true);
return true;
},
_changeFocus: function(delta) {
if (this._focus === null) {
return false;
}
return this._setFocus(this._focus + delta);
},
_clearFocus: function() {
this._focus = null;
this._focusRef = null;
},
_onselect: function (e) {
if (!e.isNormalMouseEvent()) {
// Eat right clicks, control clicks, etc., on the results. These can
// not do anything meaningful and if we let them through they'll blur
// the field and dismiss the results.
e.kill();
return;
}
var target = e.getNode('typeahead-result');
for (var ii = 0; ii < this._listNodes.length; ii++) {
if (this._listNodes[ii] === target) {
this._setFocus(ii);
this._autocomplete();
break;
}
}
this._deactivate();
e.kill();
},
_getSuffixes: function() {
return [' ', ':', ',', ')'];
},
_getCancelCharacters: function() {
// The "." character does not cancel because of projects named
// "node.js" or "blog.mycompany.com".
var defaults = ['#', '@', ',', '!', '?', '{', '}'];
return this._map[this._active].cancel || defaults;
},
_getTerminators: function() {
return [' ', ':', ',', '.', '!', '?'];
},
_getIgnoreList: function() {
return this._map[this._active].ignore || [];
},
_isTerminatedString: function(string) {
var terminators = this._getTerminators();
for (var ii = 0; ii < terminators.length; ii++) {
var term = terminators[ii];
if (string.substring(string.length - term.length) == term) {
return true;
}
}
return false;
},
_hasUnrefinableResults: function(query) {
if (!this._resultMap[this._active]) {
return false;
}
var map = this._resultMap[this._active];
for (var ii = 1; ii < query.length; ii++) {
var prefix = query.substring(0, ii);
if (map.hasOwnProperty(prefix)) {
var results = map[prefix];
// If any prefix of the query has no results, the full query also
// has no results so we can not refine them.
if (!results.length) {
return true;
}
// If there is exactly one match and the it is a prefix of the query,
// we can safely assume the user just typed out the right result
// from memory and doesn't need to refine it.
if (results.length == 1) {
// Strip the first character off, like a "#" or "@".
var result = results[0].substring(1);
if (query.length >= result.length) {
if (query.substring(0, result.length) === result) {
return true;
}
}
}
}
}
return false;
},
_trim: function(str) {
var suffixes = this._getSuffixes();
for (var ii = 0; ii < suffixes.length; ii++) {
if (str.substring(str.length - suffixes[ii].length) == suffixes[ii]) {
str = str.substring(0, str.length - suffixes[ii].length);
}
}
return str;
},
_update: function(e) {
if (!this._active) {
return;
}
var special = e.getSpecialKey();
// Deactivate if the user types escape.
if (special == 'esc') {
this._deactivate();
e.kill();
return;
}
var area = this._area;
if (e.getType() == 'keydown') {
if (special == 'up' || special == 'down') {
var delta = (special == 'up') ? -1 : +1;
if (!this._changeFocus(delta)) {
this._deactivate();
}
e.kill();
return;
}
}
// Deactivate if the user moves the cursor to the left of the assist
// range. For example, they might press the "left" arrow to move the
// cursor to the left, or click in the textarea prior to the active
// range.
var range = JX.TextAreaUtils.getSelectionRange(area);
if (range.start < this._cursorHead) {
this._deactivate();
return;
}
if (special == 'tab' || special == 'return') {
var r = e.getRawEvent();
if (r.shiftKey && special == 'tab') {
// Don't treat "Shift + Tab" as an autocomplete action. Instead,
// let it through normally so the focus shifts to the previous
// control.
this._deactivate();
return;
}
// If the user hasn't typed any text yet after typing the character
// which can summon the autocomplete, deactivate and let the keystroke
// through. For example, we hit this when a line ends with an
// autocomplete character and the user is trying to type a newline.
if (range.start == this._cursorHead) {
this._deactivate();
return;
}
// If we autocomplete, we're done. Otherwise, just eat the event. This
// happens if you type too fast and try to tab complete before results
// load.
if (this._autocomplete()) {
this._deactivate();
}
e.kill();
return;
}
// Deactivate if the user moves the cursor to the right of the assist
// range. For example, they might click later in the document. If the user
// is pressing the "right" arrow key, they are not allowed to move the
// cursor beyond the existing end of the text range. If they are pressing
// other keys, assume they're typing and allow the tail to move forward
// one character.
var margin;
if (special == 'right') {
margin = 0;
} else {
margin = 1;
}
var tail = this._cursorTail;
if ((range.start > tail + margin) || (range.end > tail + margin)) {
this._deactivate();
return;
}
this._cursorTail = Math.max(this._cursorTail, range.end);
var text = area.value.substring(
this._cursorHead,
this._cursorTail);
var pixels = JX.TextAreaUtils.getPixelDimensions(
area,
range.start,
range.end);
var x = this._pixelHead.start.x;
var y = Math.max(this._pixelHead.end.y, pixels.end.y) + 24;
// If the first character after the trigger is a space, just deactivate
// immediately. This occurs if a user types a numbered list using "#".
if (text.length && text[0] == ' ') {
this._deactivate();
return;
}
// Deactivate immediately if a user types a character that we are
// reasonably sure means they don't want to use the autocomplete. For
// example, "##" is almost certainly a header or monospaced text, not
// a project autocompletion.
var cancels = this._getCancelCharacters();
for (var ii = 0; ii < cancels.length; ii++) {
if (text.indexOf(cancels[ii]) !== -1) {
this._deactivate();
return;
}
}
var trim = this._trim(text);
// If this rule has a prefix pattern, like the "[[ document ]]" rule,
// require it match and throw it away before we begin suggesting
// results. The autocomplete remains active, it's just dormant until
// the user gives us more to work with.
var prefix = this._map[this._active].prefix;
if (prefix) {
var pattern = new RegExp(prefix);
if (!trim.match(pattern)) {
return;
}
trim = trim.replace(pattern, '');
trim = trim.trim();
}
// Store the current value now that we've finished mutating the text.
// This needs to match what we pass to the typeahead datasource.
this._value = trim;
// Deactivate immediately if the user types an ignored token like ":)",
// the smiley face emoticon. Note that we test against "text", not
// "trim", because the ignore list and suffix list can otherwise
// interact destructively.
var ignore = this._getIgnoreList();
for (ii = 0; ii < ignore.length; ii++) {
if (text.indexOf(ignore[ii]) === 0) {
this._deactivate();
return;
}
}
// If the input is terminated by a space or another word-terminating
// punctuation mark, we're going to deactivate if the results can not
// be refined by adding more words.
// The idea is that if you type "@alan ab", you're allowed to keep
// editing "ab" until you type a space, period, or other terminator,
// since you might not be sure how to spell someone's last name or the
// second word of a project.
// Once you do terminate a word, if the words you have have entered match
// nothing or match only one exact match, we can safely deactivate and
// assume you're just typing text because further words could never
// refine the result set.
var force;
if (this._isTerminatedString(text)) {
if (this._hasUnrefinableResults(text)) {
this._deactivate();
return;
}
force = true;
} else {
force = false;
}
this._datasource.didChange(trim, force);
this._x = x;
this._y = y;
var hint = trim;
if (hint.length) {
// We only show the autocompleter after the user types at least one
// character. For example, "@" does not trigger it, but "@d" does.
this._visible = true;
} else {
hint = this._getSpec().hintText;
}
var echo = this._getEchoNode();
JX.DOM.setContent(echo, hint);
this._redraw();
},
_redraw: function() {
if (!this._visible) {
return;
}
var node = this._getNode();
JX.DOM.show(node);
var p = new JX.Vector(this._x, this._y);
var s = JX.Vector.getScroll();
var v = JX.Vector.getViewport();
// If the menu would run off the bottom of the screen when showing the
// maximum number of possible choices, put it above instead. We're doing
// this based on the maximum size so the menu doesn't jump up and down
// as results arrive.
var option_height = 30;
var extra_margin = 24;
if ((s.y + v.y) < (p.y + (5 * option_height) + extra_margin)) {
var d = JX.Vector.getDim(node);
p.y = p.y - d.y - 36;
}
p.setPos(node);
},
_autocomplete: function() {
if (this._focus === null) {
return false;
}
var area = this._area;
var head = this._cursorHead;
var tail = this._cursorTail;
var text = area.value;
var ref = this._focusRef;
var result = this._datasource.getResult(ref);
if (!result) {
return false;
}
ref = result.autocomplete;
if (!ref || !ref.length) {
return false;
}
// If the user types a string like "@username:" (with a trailing colon),
// then presses tab or return to pick the completion, don't destroy the
// trailing character.
var suffixes = this._getSuffixes();
var value = this._value;
var found_suffix = false;
for (var ii = 0; ii < suffixes.length; ii++) {
var last = value.substring(value.length - suffixes[ii].length);
if (last == suffixes[ii]) {
ref += suffixes[ii];
found_suffix = true;
break;
}
}
// If we didn't find an existing suffix, add a space.
if (!found_suffix) {
ref = ref + ' ';
}
area.value = text.substring(0, head - 1) + ref + text.substring(tail);
var end = head + ref.length;
JX.TextAreaUtils.setSelectionRange(area, end, end);
return true;
},
_getNode: function() {
if (!this._node) {
var head = this._getHeadNode();
var list = this._getListNode();
this._node = JX.$N(
'div',
{
className: 'phuix-autocomplete',
style: {
display: 'none'
}
},
[head, list]);
JX.DOM.hide(this._node);
document.body.appendChild(this._node);
}
return this._node;
},
_getHeadNode: function() {
if (!this._headNode) {
this._headNode = JX.$N(
'div',
{
className: 'phuix-autocomplete-head'
},
[
this._getPromptNode(),
this._getEchoNode()
]);
}
return this._headNode;
},
_getPromptNode: function() {
if (!this._promptNode) {
this._promptNode = JX.$N(
'span',
{
className: 'phuix-autocomplete-prompt',
});
}
return this._promptNode;
},
_getEchoNode: function() {
if (!this._echoNode) {
this._echoNode = JX.$N(
'span',
{
className: 'phuix-autocomplete-echo'
});
}
return this._echoNode;
},
_getListNode: function() {
if (!this._listNode) {
this._listNode = JX.$N(
'div',
{
className: 'phuix-autocomplete-list'
});
}
return this._listNode;
}
}
});

Event Timeline